com.google.common.geometry.S2Polygon.java Source code

Java tutorial

Introduction

Here is the source code for com.google.common.geometry.S2Polygon.java

Source

/*
 * Copyright 2006 Google Inc.
 *
 * Licensed 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 com.google.common.geometry;

import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multiset;
import com.google.common.collect.TreeMultimap;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

/**
 * An S2Polygon is an S2Region object that represents a polygon. A polygon
 * consists of zero or more {@link S2Loop loops} representing "shells" and
 * "holes". All loops should be oriented CCW, i.e. the shell or hole is on the
 * left side of the loop. Loops may be specified in any order. A point is
 * defined to be inside the polygon if it is contained by an odd number of
 * loops.
 *
 *  Polygons have the following restrictions:
 *
 *  - Loops may not cross, i.e. the boundary of a loop may not intersect both
 * the interior and exterior of any other loop.
 *
 *  - Loops may not share edges, i.e. if a loop contains an edge AB, then no
 * other loop may contain AB or BA.
 *
 *  - No loop may cover more than half the area of the sphere. This ensures that
 * no loop properly contains the complement of any other loop, even if the loops
 * are from different polygons. (Loops that represent exact hemispheres are
 * allowed.)
 *
 *  Loops may share vertices, however no vertex may appear twice in a single
 * loop.
 *
 */
public final strictfp class S2Polygon implements S2Region, Comparable<S2Polygon> {
    private static final Logger log = Logger.getLogger(S2Polygon.class.getCanonicalName());

    private List<S2Loop> loops;

    private S2LatLngRect bound;
    private boolean hasHoles;
    private int numVertices;

    /**
     * Creates an empty polygon that should be initialized by calling Init().
     */
    public S2Polygon() {
        this.loops = Lists.newArrayList();
        this.bound = S2LatLngRect.empty();
        this.hasHoles = false;
        this.numVertices = 0;
    }

    /**
     * Convenience constructor that calls Init() with the given loops. Clears the
     * given list.
     */
    public S2Polygon(List<S2Loop> loops) {
        this.loops = Lists.newArrayList();
        this.bound = S2LatLngRect.empty();

        init(loops);
    }

    /**
     * Copy constructor.
     */
    public S2Polygon(S2Loop loop) {
        this.loops = Lists.newArrayList();
        this.bound = loop.getRectBound();
        this.hasHoles = false;
        this.numVertices = loop.numVertices();

        loops.add(loop);
    }

    /**
     * Copy constructor.
     */
    public S2Polygon(S2Polygon src) {
        this.loops = Lists.newArrayList();
        this.bound = src.getRectBound();
        this.hasHoles = src.hasHoles;
        this.numVertices = src.numVertices;

        for (int i = 0; i < src.numLoops(); ++i) {
            loops.add(new S2Loop(src.loop(i)));
        }
    }

    /**
     * Comparator (needed by Comparable interface). For two polygons to be
     * compared as equal: - the must have the same number of loops; - the loops
     * must be ordered in the same way (this is guaranteed by the total ordering
     * imposed by sortValueLoops). - loops must be logically equivalent (even if
     * ordered with a different starting point, e.g. ABCD and BCDA).
     */
    @Override
    public int compareTo(S2Polygon other) {
        // If number of loops differ, use that.
        if (this.numLoops() != other.numLoops()) {
            return this.numLoops() - other.numLoops();
        }
        for (int i = 0; i < this.numLoops(); ++i) {
            int compare = this.loops.get(i).compareTo(other.loops.get(i));
            if (compare != 0) {
                return compare;
            }
        }
        return 0;
    }

    /**
     * Initialize a polygon by taking ownership of the given loops and clearing
     * the given list. This method figures out the loop nesting hierarchy and then
     * reorders the loops by following a preorder traversal. This implies that
     * each loop is immediately followed by its descendants in the nesting
     * hierarchy. (See also getParent and getLastDescendant.)
     */
    public void init(List<S2Loop> loops) {
        // assert isValid(loops);
        // assert (this.loops.isEmpty());

        Map<S2Loop, List<S2Loop>> loopMap = Maps.newHashMap();
        // Yes, a null key is valid. It is used here to refer to the root of the
        // loopMap
        loopMap.put(null, Lists.<S2Loop>newArrayList());

        for (S2Loop loop : loops) {
            insertLoop(loop, null, loopMap);
            this.numVertices += loop.numVertices();
        }
        loops.clear();

        // Sort all of the lists of loops; in this way we guarantee a total ordering
        // on loops in the polygon. Loops will be sorted by their natural ordering,
        // while also preserving the requirement that each loop is immediately
        // followed by its descendants in the nesting hierarchy.
        //
        // TODO(andriy): as per kirilll in CL 18750833 code review comments:
        // This should work for now, but I think it's possible to guarantee the
        // correct order inside insertLoop by searching for the correct position in
        // the children list before inserting.
        sortValueLoops(loopMap);

        // Reorder the loops in depth-first traversal order.
        // Starting at null == starting at the root
        initLoop(null, -1, loopMap);

        // TODO(dbeaumont): Add tests or preconditions for these asserts (here and elesewhere).
        // forall i != j : containsChild(loop(i), loop(j), loopMap) == loop(i).containsNested(loop(j)));

        // Compute the bounding rectangle of the entire polygon.
        hasHoles = false;
        bound = S2LatLngRect.empty();
        for (int i = 0; i < numLoops(); ++i) {
            if (loop(i).sign() < 0) {
                hasHoles = true;
            } else {
                bound = bound.union(loop(i).getRectBound());
            }
        }
    }

    /**
     * Release ownership of the loops of this polygon by appending them to the
     * given list. Resets the polygon to be empty.
     */
    public void release(List<S2Loop> loops) {
        loops.addAll(this.loops);
        this.loops.clear();
        bound = S2LatLngRect.empty();
        hasHoles = false;
        numVertices = 0;
    }

    /**
     * Return true if the given loops form a valid polygon. Assumes that that all
     * of the given loops have already been validated.
     */
    public static boolean isValid(final List<S2Loop> loops) {
        // If a loop contains an edge AB, then no other loop may contain AB or BA.
        // We only need this test if there are at least two loops, assuming that
        // each loop has already been validated.
        if (loops.size() > 1) {
            Map<UndirectedEdge, LoopVertexIndexPair> edges = Maps.newHashMap();
            for (int i = 0; i < loops.size(); ++i) {
                S2Loop lp = loops.get(i);
                for (int j = 0; j < lp.numVertices(); ++j) {
                    UndirectedEdge key = new UndirectedEdge(lp.vertex(j), lp.vertex(j + 1));
                    LoopVertexIndexPair value = new LoopVertexIndexPair(i, j);
                    if (edges.containsKey(key)) {
                        LoopVertexIndexPair other = edges.get(key);
                        log.info("Duplicate edge: loop " + i + ", edge " + j + " and loop " + other.getLoopIndex()
                                + ", edge " + other.getVertexIndex());
                        return false;
                    } else {
                        edges.put(key, value);
                    }
                }
            }
        }

        // Verify that no loop covers more than half of the sphere, and that no
        // two loops cross.
        for (int i = 0; i < loops.size(); ++i) {
            if (!loops.get(i).isNormalized()) {
                log.info("Loop " + i + " encloses more than half the sphere");
                return false;
            }
            for (int j = i + 1; j < loops.size(); ++j) {
                // This test not only checks for edge crossings, it also detects
                // cases where the two boundaries cross at a shared vertex.
                if (loops.get(i).containsOrCrosses(loops.get(j)) < 0) {
                    log.info("Loop " + i + " crosses loop " + j);
                    return false;
                }
            }
        }
        return true;
    }

    public int numLoops() {
        return loops.size();
    }

    public S2Loop loop(int k) {
        return loops.get(k);
    }

    /**
     * Return the index of the parent of loop k, or -1 if it has no parent.
     */
    public int getParent(int k) {
        int depth = loop(k).depth();
        if (depth == 0) {
            return -1; // Optimization.
        }
        while (--k >= 0 && loop(k).depth() >= depth) {
            // spin
        }
        return k;
    }

    /**
     * Return the index of the last loop that is contained within loop k. Returns
     * num_loops() - 1 if k < 0. Note that loops are indexed according to a
     * preorder traversal of the nesting hierarchy, so the immediate children of
     * loop k can be found by iterating over loops (k+1)..getLastDescendant(k) and
     * selecting those whose depth is equal to (loop(k).depth() + 1).
     */
    public int getLastDescendant(int k) {
        if (k < 0) {
            return numLoops() - 1;
        }
        int depth = loop(k).depth();
        while (++k < numLoops() && loop(k).depth() > depth) {
            // spin
        }
        return k - 1;
    }

    private S2AreaCentroid getAreaCentroid(boolean doCentroid) {
        double areaSum = 0;
        S2Point centroidSum = new S2Point(0, 0, 0);
        for (int i = 0; i < numLoops(); ++i) {
            S2AreaCentroid areaCentroid = doCentroid ? loop(i).getAreaAndCentroid() : null;
            double loopArea = doCentroid ? areaCentroid.getArea() : loop(i).getArea();

            int loopSign = loop(i).sign();
            areaSum += loopSign * loopArea;
            if (doCentroid) {
                S2Point currentCentroid = areaCentroid.getCentroid();
                centroidSum = new S2Point(centroidSum.x + loopSign * currentCentroid.x,
                        centroidSum.y + loopSign * currentCentroid.y, centroidSum.z + loopSign * currentCentroid.z);
            }
        }

        return new S2AreaCentroid(areaSum, doCentroid ? centroidSum : null);
    }

    /**
     * Return the area of the polygon interior, i.e. the region on the left side
     * of an odd number of loops (this value return value is between 0 and 4*Pi)
     * and the true centroid of the polygon multiplied by the area of the polygon
     * (see s2.h for details on centroids). Note that the centroid may not be
     * contained by the polygon.
     */
    public S2AreaCentroid getAreaAndCentroid() {
        return getAreaCentroid(true);
    }

    /**
     * Return the area of the polygon interior, i.e. the region on the left side
     * of an odd number of loops. The return value is between 0 and 4*Pi.
     */
    public double getArea() {
        return getAreaCentroid(false).getArea();
    }

    /**
     * Return the true centroid of the polygon multiplied by the area of the
     * polygon (see s2.h for details on centroids). Note that the centroid may not
     * be contained by the polygon.
     */
    public S2Point getCentroid() {
        return getAreaCentroid(true).getCentroid();
    }

    /**
     * Returns the shortest distance from a point P to this polygon, given as the
     * angle formed between P, the origin and the nearest point on the polygon to
     * P. This angle in radians is equivalent to the arclength along the unit
     * sphere.
     *
     * If the point is contained inside the polygon, the distance returned is 0.
     */
    public S1Angle getDistance(S2Point p) {
        if (contains(p)) {
            return S1Angle.radians(0);
        }

        // The furthest point from p on the sphere is its antipode, which is an
        // angle of PI radians. This is an upper bound on the angle.
        S1Angle minDistance = S1Angle.radians(Math.PI);
        for (int i = 0; i < numLoops(); i++) {
            minDistance = S1Angle.min(minDistance, loop(i).getDistance(p));
        }

        return minDistance;
    }

    /**
     * Return true if this polygon contains the given other polygon, i.e. if
     * polygon A contains all points contained by polygon B.
     */
    public boolean contains(S2Polygon b) {
        // If both polygons have one loop, use the more efficient S2Loop method.
        // Note that S2Loop.contains does its own bounding rectangle check.
        if (numLoops() == 1 && b.numLoops() == 1) {
            return loop(0).contains(b.loop(0));
        }

        // Otherwise if neither polygon has holes, we can still use the more
        // efficient S2Loop::Contains method (rather than ContainsOrCrosses),
        // but it's worthwhile to do our own bounds check first.
        if (!bound.contains(b.getRectBound())) {
            // If the union of the bounding boxes spans the full longitude range,
            // it is still possible that polygon A contains B. (This is only
            // possible if at least one polygon has multiple shells.)
            if (!bound.lng().union(b.getRectBound().lng()).isFull()) {
                return false;
            }
        }
        if (!hasHoles && !b.hasHoles) {
            for (int j = 0; j < b.numLoops(); ++j) {
                if (!anyLoopContains(b.loop(j))) {
                    return false;
                }
            }
            return true;
        }

        // This could be implemented more efficiently for polygons with lots of
        // holes by keeping a copy of the LoopMap computed during initialization.
        // However, in practice most polygons are one loop, and multiloop polygons
        // tend to consist of many shells rather than holes. In any case, the real
        // way to get more efficiency is to implement a sub-quadratic algorithm
        // such as building a trapezoidal map.

        // Every shell of B must be contained by an odd number of loops of A,
        // and every hole of A must be contained by an even number of loops of B.
        return containsAllShells(b) && b.excludesAllHoles(this);
    }

    /**
     * Return true if this polygon intersects the given other polygon, i.e. if
     * there is a point that is contained by both polygons.
     */
    public boolean intersects(S2Polygon b) {
        // A.intersects(B) if and only if !complement(A).contains(B). However,
        // implementing a complement() operation is trickier than it sounds,
        // and in any case it's more efficient to test for intersection directly.

        // If both polygons have one loop, use the more efficient S2Loop method.
        // Note that S2Loop.intersects does its own bounding rectangle check.
        if (numLoops() == 1 && b.numLoops() == 1) {
            return loop(0).intersects(b.loop(0));
        }

        // Otherwise if neither polygon has holes, we can still use the more
        // efficient S2Loop.intersects method. The polygons intersect if and
        // only if some pair of loop regions intersect.
        if (!bound.intersects(b.getRectBound())) {
            return false;
        }
        if (!hasHoles && !b.hasHoles) {
            for (int i = 0; i < numLoops(); ++i) {
                for (int j = 0; j < b.numLoops(); ++j) {
                    if (loop(i).intersects(b.loop(j))) {
                        return true;
                    }
                }
            }
            return false;
        }

        // Otherwise if any shell of B is contained by an odd number of loops of A,
        // or any shell of A is contained by an odd number of loops of B, there is
        // an intersection.
        return intersectsAnyShell(b) || b.intersectsAnyShell(this);
    }

    /**
     *  Indexing structure to efficiently clipEdge() of a polygon. This is an
     * abstract class because we need to use if for both polygons (for
     * initToIntersection() and friends) and for sets of lists of points (for
     * initToSimplified()).
     *
     *  Usage -- in your subclass, create an array of vertex counts for each loop
     * in the loop sequence and pass it to this constructor. Overwrite
     * edgeFromTo(), calling decodeIndex() and use the resulting two indices to
     * access your accessing vertices.
     */
    private abstract static class S2LoopSequenceIndex extends S2EdgeIndex {
        /** Map from the unidimensional edge index to the loop this edge belongs to. */
        private final int[] indexToLoop;

        /**
         * Reverse of indexToLoop: maps a loop index to the unidimensional index
         * of the first edge in the loop.
         */
        private final int[] loopToFirstIndex;

        /**
         * Must be called by each subclass with the array of vertices per loop. The
         * length of the array is the number of loops, and the <code>i</code>
         * <sup>th</sup> loop's vertex count is in the <code>i</code>
         * <sup>th</sup> index of the array.
         */
        public S2LoopSequenceIndex(int[] numVertices) {
            int totalEdges = 0;
            for (int edges : numVertices) {
                totalEdges += edges;
            }
            indexToLoop = new int[totalEdges];
            loopToFirstIndex = new int[numVertices.length];

            totalEdges = 0;
            for (int j = 0; j < numVertices.length; j++) {
                loopToFirstIndex[j] = totalEdges;
                for (int i = 0; i < numVertices[j]; i++) {
                    indexToLoop[totalEdges] = j;
                    totalEdges++;
                }
            }
        }

        public final LoopVertexIndexPair decodeIndex(int index) {
            int loopIndex = indexToLoop[index];
            int vertexInLoop = index - loopToFirstIndex[loopIndex];
            return new LoopVertexIndexPair(loopIndex, vertexInLoop);
        }

        // It is faster to return both vertices at once. It makes a difference
        // for small polygons.
        public abstract S2Edge edgeFromTo(int index);

        @Override
        public final int getNumEdges() {
            return indexToLoop.length;
        }

        @Override
        public S2Point edgeFrom(int index) {
            S2Edge fromTo = edgeFromTo(index);
            S2Point from = fromTo.getStart();
            return from;
        }

        @Override
        protected S2Point edgeTo(int index) {
            S2Edge fromTo = edgeFromTo(index);
            S2Point to = fromTo.getEnd();
            return to;
        }
    }

    // Indexing structure for an S2Polygon.
    private static final class S2PolygonIndex extends S2LoopSequenceIndex {
        private final S2Polygon poly;
        private final boolean reverse;

        private static int[] getVertices(S2Polygon poly) {
            int[] vertices = new int[poly.numLoops()];
            for (int i = 0; i < vertices.length; i++) {
                vertices[i] = poly.loop(i).numVertices();
            }
            return vertices;
        }

        public S2PolygonIndex(S2Polygon poly, boolean reverse) {
            super(getVertices(poly));
            this.poly = poly;
            this.reverse = reverse;
        }

        @Override
        public S2Edge edgeFromTo(int index) {
            LoopVertexIndexPair indices = decodeIndex(index);
            int loopIndex = indices.getLoopIndex();
            int vertexInLoop = indices.getVertexIndex();
            S2Loop loop = poly.loop(loopIndex);
            int fromIndex;
            int toIndex;
            if (loop.isHole() ^ reverse) {
                fromIndex = loop.numVertices() - 1 - vertexInLoop;
                toIndex = 2 * loop.numVertices() - 2 - vertexInLoop;
            } else {
                fromIndex = vertexInLoop;
                toIndex = vertexInLoop + 1;
            }
            S2Point from = loop.vertex(fromIndex);
            S2Point to = loop.vertex(toIndex);
            return new S2Edge(from, to);
        }
    }

    private static void addIntersection(S2Point a0, S2Point a1, S2Point b0, S2Point b1, boolean addSharedEdges,
            int crossing, List<ParametrizedS2Point> intersections) {
        if (crossing > 0) {
            // There is a proper edge crossing.
            S2Point x = S2EdgeUtil.getIntersection(a0, a1, b0, b1);
            double t = S2EdgeUtil.getDistanceFraction(x, a0, a1);
            intersections.add(new ParametrizedS2Point(t, x));
        } else if (S2EdgeUtil.vertexCrossing(a0, a1, b0, b1)) {
            // There is a crossing at one of the vertices. The basic rule is simple:
            // if a0 equals one of the "b" vertices, the crossing occurs at t=0;
            // otherwise, it occurs at t=1.
            //
            // This has the effect that when two symmetric edges are encountered (an
            // edge an its reverse), neither one is included in the output. When two
            // duplicate edges are encountered, both are included in the output. The
            // "addSharedEdges" flag allows one of these two copies to be removed by
            // changing its intersection parameter from 0 to 1.
            double t = (a0 == b0 || a0 == b1) ? 0 : 1;
            if (!addSharedEdges && a1 == b1) {
                t = 1;
            }
            intersections.add(new ParametrizedS2Point(t, t == 0 ? a0 : a1));
        }
    }

    /**
     * Find all points where the polygon B intersects the edge (a0,a1), and add
     * the corresponding parameter values (in the range [0,1]) to "intersections".
     */
    private static void clipEdge(final S2Point a0, final S2Point a1, S2LoopSequenceIndex bIndex,
            boolean addSharedEdges, List<ParametrizedS2Point> intersections) {
        S2LoopSequenceIndex.DataEdgeIterator it = new S2LoopSequenceIndex.DataEdgeIterator(bIndex);
        it.getCandidates(a0, a1);
        S2EdgeUtil.EdgeCrosser crosser = new S2EdgeUtil.EdgeCrosser(a0, a1, a0);
        S2Point from = null;
        S2Point to = null;
        for (; it.hasNext(); it.next()) {
            S2Point previousTo = to;
            S2Edge fromTo = bIndex.edgeFromTo(it.index());
            from = fromTo.getStart();
            to = fromTo.getEnd();
            if (previousTo != from) {
                crosser.restartAt(from);
            }
            int crossing = crosser.robustCrossing(to);
            if (crossing < 0) {
                continue;
            }
            addIntersection(a0, a1, from, to, addSharedEdges, crossing, intersections);
        }
    }

    /**
     * Clip the boundary of A to the interior of B, and add the resulting edges to
     * "builder". Shells are directed CCW and holes are directed clockwise, unless
     * "reverseA" or "reverseB" is true in which case these directions in the
     * corresponding polygon are reversed. If "invertB" is true, the boundary of A
     * is clipped to the exterior rather than the interior of B. If
     * "adSharedEdges" is true, then the output will include any edges that are
     * shared between A and B (both edges must be in the same direction after any
     * edge reversals are taken into account).
     */
    private static void clipBoundary(final S2Polygon a, boolean reverseA, final S2Polygon b, boolean reverseB,
            boolean invertB, boolean addSharedEdges, S2PolygonBuilder builder) {
        S2PolygonIndex bIndex = new S2PolygonIndex(b, reverseB);
        bIndex.predictAdditionalCalls(a.getNumVertices());

        List<ParametrizedS2Point> intersections = Lists.newArrayList();
        for (S2Loop aLoop : a.loops) {
            int n = aLoop.numVertices();
            int dir = (aLoop.isHole() ^ reverseA) ? -1 : 1;
            boolean inside = b.contains(aLoop.vertex(0)) ^ invertB;
            for (int j = (dir > 0) ? 0 : n; n > 0; --n, j += dir) {
                S2Point a0 = aLoop.vertex(j);
                S2Point a1 = aLoop.vertex(j + dir);
                intersections.clear();
                clipEdge(a0, a1, bIndex, addSharedEdges, intersections);

                if (inside) {
                    intersections.add(new ParametrizedS2Point(0.0, a0));
                }
                inside = ((intersections.size() & 0x1) == 0x1);
                // assert ((b.contains(a1) ^ invertB) == inside);
                if (inside) {
                    intersections.add(new ParametrizedS2Point(1.0, a1));
                }

                // Remove duplicates and produce a list of unique intersections.
                Collections.sort(intersections);
                for (int size = intersections.size(), i = 1; i < size; i += 2) {
                    builder.addEdge(intersections.get(i - 1).getPoint(), intersections.get(i).getPoint());
                }
            }
        }
    }

    /**
     * Returns total number of vertices in all loops.
     */
    public int getNumVertices() {
        return this.numVertices;
    }

    /**
     * Initialize this polygon to the intersection, union, or difference (A - B)
     * of the given two polygons. The "vertexMergeRadius" determines how close two
     * vertices must be to be merged together and how close a vertex must be to an
     * edge in order to be spliced into it (see S2PolygonBuilder for details). By
     * default, the merge radius is just large enough to compensate for errors
     * that occur when computing intersection points between edges
     * (S2EdgeUtil.DEFAULT_INTERSECTION_TOLERANCE).
     *
     *  If you are going to convert the resulting polygon to a lower-precision
     * format, it is necessary to increase the merge radius in order to get a
     * valid result after rounding (i.e. no duplicate vertices, etc). For example,
     * if you are going to convert them to geostore.PolygonProto format, then
     * S1Angle.e7(1) is a good value for "vertex_merge_radius".
     */
    public void initToIntersection(final S2Polygon a, final S2Polygon b) {
        initToIntersectionSloppy(a, b, S2EdgeUtil.DEFAULT_INTERSECTION_TOLERANCE);
    }

    public void initToIntersectionSloppy(final S2Polygon a, final S2Polygon b, S1Angle vertexMergeRadius) {
        Preconditions.checkState(numLoops() == 0);
        if (!a.bound.intersects(b.bound)) {
            return;
        }

        // We want the boundary of A clipped to the interior of B,
        // plus the boundary of B clipped to the interior of A,
        // plus one copy of any directed edges that are in both boundaries.

        S2PolygonBuilder.Options options = S2PolygonBuilder.Options.DIRECTED_XOR;
        options.setMergeDistance(vertexMergeRadius);
        S2PolygonBuilder builder = new S2PolygonBuilder(options);
        clipBoundary(a, false, b, false, false, true, builder);
        clipBoundary(b, false, a, false, false, false, builder);
        if (!builder.assemblePolygon(this, null)) {
            // TODO (andriy): do something more meaningful here.
            log.severe("Bad directed edges");
        }
    }

    public void initToUnion(final S2Polygon a, final S2Polygon b) {
        initToUnionSloppy(a, b, S2EdgeUtil.DEFAULT_INTERSECTION_TOLERANCE);
    }

    public void initToUnionSloppy(final S2Polygon a, final S2Polygon b, S1Angle vertexMergeRadius) {
        Preconditions.checkState(numLoops() == 0);

        // We want the boundary of A clipped to the exterior of B,
        // plus the boundary of B clipped to the exterior of A,
        // plus one copy of any directed edges that are in both boundaries.

        S2PolygonBuilder.Options options = S2PolygonBuilder.Options.DIRECTED_XOR;
        options.setMergeDistance(vertexMergeRadius);
        S2PolygonBuilder builder = new S2PolygonBuilder(options);
        clipBoundary(a, false, b, false, true, true, builder);
        clipBoundary(b, false, a, false, true, false, builder);
        if (!builder.assemblePolygon(this, null)) {
            // TODO(andriy): do something more meaningful here.
            log.severe("Bad directed edges");
        }
    }

    /**
     * Return a polygon which is the union of the given polygons. Note: clears the
     * List!
     */
    public static S2Polygon destructiveUnion(List<S2Polygon> polygons) {
        return destructiveUnionSloppy(polygons, S2EdgeUtil.DEFAULT_INTERSECTION_TOLERANCE);
    }

    /**
     * Return a polygon which is the union of the given polygons; combines
     * vertices that form edges that are almost identical, as defined by
     * vertexMergeRadius. Note: clears the List!
     */
    public static S2Polygon destructiveUnionSloppy(List<S2Polygon> polygons, S1Angle vertexMergeRadius) {
        // Effectively create a priority queue of polygons in order of number of
        // vertices. Repeatedly union the two smallest polygons and add the result
        // to the queue until we have a single polygon to return.

        // map: # of vertices -> polygon
        TreeMultimap<Integer, S2Polygon> queue = TreeMultimap.create();

        for (S2Polygon polygon : polygons) {
            queue.put(polygon.getNumVertices(), polygon);
        }
        polygons.clear();

        Set<Map.Entry<Integer, S2Polygon>> queueSet = queue.entries();
        while (queueSet.size() > 1) {
            // Pop two simplest polygons from queue.
            queueSet = queue.entries();
            Iterator<Map.Entry<Integer, S2Polygon>> smallestIter = queueSet.iterator();

            Map.Entry<Integer, S2Polygon> smallest = smallestIter.next();
            int aSize = smallest.getKey().intValue();
            S2Polygon aPolygon = smallest.getValue();
            smallestIter.remove();

            smallest = smallestIter.next();
            int bSize = smallest.getKey().intValue();
            S2Polygon bPolygon = smallest.getValue();
            smallestIter.remove();

            // Union and add result back to queue.
            S2Polygon unionPolygon = new S2Polygon();
            unionPolygon.initToUnionSloppy(aPolygon, bPolygon, vertexMergeRadius);
            int unionSize = aSize + bSize;
            queue.put(unionSize, unionPolygon);
            // We assume that the number of vertices in the union polygon is the
            // sum of the number of vertices in the original polygons, which is not
            // always true, but will almost always be a decent approximation, and
            // faster than recomputing.
        }

        if (queue.isEmpty()) {
            return new S2Polygon();
        } else {
            return queue.get(queue.asMap().firstKey()).first();
        }
    }

    public boolean isNormalized() {
        Multiset<S2Point> vertices = HashMultiset.<S2Point>create();
        S2Loop lastParent = null;
        for (int i = 0; i < numLoops(); ++i) {
            S2Loop child = loop(i);
            if (child.depth() == 0) {
                continue;
            }
            S2Loop parent = loop(getParent(i));
            if (parent != lastParent) {
                vertices.clear();
                for (int j = 0; j < parent.numVertices(); ++j) {
                    vertices.add(parent.vertex(j));
                }
                lastParent = parent;
            }
            int count = 0;
            for (int j = 0; j < child.numVertices(); ++j) {
                if (vertices.count(child.vertex(j)) > 0) {
                    ++count;
                }
            }
            if (count > 1) {
                return false;
            }
        }
        return true;
    }

    /**
     * Return true if two polygons have the same boundary except for vertex
     * perturbations. Both polygons must have loops with the same cyclic vertex
     * order and the same nesting hierarchy, but the vertex locations are allowed
     * to differ by up to "max_error". Note: This method mostly useful only for
     * testing purposes.
     */
    boolean boundaryApproxEquals(S2Polygon b, double maxError) {
        if (numLoops() != b.numLoops()) {
            log.severe("!= loops: " + Integer.toString(numLoops()) + " vs. " + Integer.toString(b.numLoops()));
            return false;
        }

        // For now, we assume that there is at most one candidate match for each
        // loop. (So far this method is just used for testing.)
        for (int i = 0; i < numLoops(); ++i) {
            S2Loop aLoop = loop(i);
            boolean success = false;
            for (int j = 0; j < numLoops(); ++j) {
                S2Loop bLoop = b.loop(j);
                if (bLoop.depth() == aLoop.depth() && bLoop.boundaryApproxEquals(aLoop, maxError)) {
                    success = true;
                    break;
                }
            }
            if (!success) {
                return false;
            }
        }
        return true;
    }

    // S2Region interface (see S2Region.java for details):

    /** Return a bounding spherical cap. */
    @Override
    public S2Cap getCapBound() {
        return bound.getCapBound();
    }

    /** Return a bounding latitude-longitude rectangle. */
    @Override
    public S2LatLngRect getRectBound() {
        return bound;
    }

    /**
     * If this method returns true, the region completely contains the given cell.
     * Otherwise, either the region does not contain the cell or the containment
     * relationship could not be determined.
     */
    @Override
    public boolean contains(S2Cell cell) {
        if (numLoops() == 1) {
            return loop(0).contains(cell);
        }
        S2LatLngRect cellBound = cell.getRectBound();
        if (!bound.contains(cellBound)) {
            return false;
        }

        S2Loop cellLoop = new S2Loop(cell, cellBound);
        S2Polygon cellPoly = new S2Polygon(cellLoop);
        return contains(cellPoly);
    }

    /**
     * If this method returns false, the region does not intersect the given cell.
     * Otherwise, either region intersects the cell, or the intersection
     * relationship could not be determined.
     */
    @Override
    public boolean mayIntersect(S2Cell cell) {
        if (numLoops() == 1) {
            return loop(0).mayIntersect(cell);
        }
        S2LatLngRect cellBound = cell.getRectBound();
        if (!bound.intersects(cellBound)) {
            return false;
        }

        S2Loop cellLoop = new S2Loop(cell, cellBound);
        S2Polygon cellPoly = new S2Polygon(cellLoop);
        return intersects(cellPoly);
    }

    /**
     * The point 'p' does not need to be normalized.
     */
    public boolean contains(S2Point p) {
        if (numLoops() == 1) {
            return loop(0).contains(p); // Optimization.
        }
        if (!bound.contains(p)) {
            return false;
        }
        boolean inside = false;
        for (int i = 0; i < numLoops(); ++i) {
            inside ^= loop(i).contains(p);
            if (inside && !hasHoles) {
                break; // Shells are disjoint.
            }
        }
        return inside;
    }

    // For each map entry, sorts the value list.
    private static void sortValueLoops(Map<S2Loop, List<S2Loop>> loopMap) {
        for (S2Loop key : loopMap.keySet()) {
            Collections.sort(loopMap.get(key));
        }
    }

    private static void insertLoop(S2Loop newLoop, S2Loop parent, Map<S2Loop, List<S2Loop>> loopMap) {
        List<S2Loop> children = loopMap.get(parent);

        if (children == null) {
            children = Lists.newArrayList();
            loopMap.put(parent, children);
        }

        for (S2Loop child : children) {
            if (child.containsNested(newLoop)) {
                insertLoop(newLoop, child, loopMap);
                return;
            }
        }

        // No loop may contain the complement of another loop. (Handling this case
        // is significantly more complicated.)
        // assert (parent == null || !newLoop.containsNested(parent));

        // Some of the children of the parent loop may now be children of
        // the new loop.
        List<S2Loop> newChildren = loopMap.get(newLoop);
        for (int i = 0; i < children.size();) {
            S2Loop child = children.get(i);
            if (newLoop.containsNested(child)) {
                if (newChildren == null) {
                    newChildren = Lists.newArrayList();
                    loopMap.put(newLoop, newChildren);
                }
                newChildren.add(child);
                children.remove(i);
            } else {
                ++i;
            }
        }
        children.add(newLoop);
    }

    private void initLoop(S2Loop loop, int depth, Map<S2Loop, List<S2Loop>> loopMap) {
        if (loop != null) {
            loop.setDepth(depth);
            loops.add(loop);
        }
        List<S2Loop> children = loopMap.get(loop);
        if (children != null) {
            for (S2Loop child : children) {
                initLoop(child, depth + 1, loopMap);
            }
        }
    }

    private int containsOrCrosses(S2Loop b) {
        boolean inside = false;
        for (int i = 0; i < numLoops(); ++i) {
            int result = loop(i).containsOrCrosses(b);
            if (result < 0) {
                return -1; // The loop boundaries intersect.
            }
            if (result > 0) {
                inside ^= true;
            }
        }
        return inside ? 1 : 0; // True if loop B is contained by the polygon.
    }

    /** Return true if any loop contains the given loop. */
    private boolean anyLoopContains(S2Loop b) {
        for (int i = 0; i < numLoops(); ++i) {
            if (loop(i).contains(b)) {
                return true;
            }
        }
        return false;
    }

    /** Return true if this polygon (A) contains all the shells of B. */
    private boolean containsAllShells(S2Polygon b) {
        for (int j = 0; j < b.numLoops(); ++j) {
            if (b.loop(j).sign() < 0) {
                continue;
            }
            if (containsOrCrosses(b.loop(j)) <= 0) {
                // Shell of B is not contained by A, or the boundaries intersect.
                return false;
            }
        }
        return true;
    }

    /**
     * Return true if this polygon (A) excludes (i.e. does not intersect) all
     * holes of B.
     */
    private boolean excludesAllHoles(S2Polygon b) {
        for (int j = 0; j < b.numLoops(); ++j) {
            if (b.loop(j).sign() > 0) {
                continue;
            }
            if (containsOrCrosses(b.loop(j)) != 0) {
                // Hole of B is contained by A, or the boundaries intersect.
                return false;
            }
        }
        return true;
    }

    /** Return true if this polygon (A) intersects any shell of B. */
    private boolean intersectsAnyShell(S2Polygon b) {
        for (int j = 0; j < b.numLoops(); ++j) {
            if (b.loop(j).sign() < 0) {
                continue;
            }
            if (containsOrCrosses(b.loop(j)) != 0) {
                // Shell of B is contained by A, or the boundaries intersect.
                return true;
            }
        }
        return false;
    }

    /**
     * A human readable representation of the polygon
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Polygon: (").append(numLoops()).append(") loops:\n");
        for (int i = 0; i < numLoops(); ++i) {
            S2Loop s2Loop = loop(i);
            sb.append("loop <\n");
            for (int v = 0; v < s2Loop.numVertices(); ++v) {
                S2Point s2Point = s2Loop.vertex(v);
                sb.append(s2Point.toDegreesString());
                sb.append("\n"); // end of vertex
            }
            sb.append(">\n"); // end of loop
        }
        return sb.toString();
    }

    private static final class UndirectedEdge {
        // Note: An UndirectedEdge and an S2Edge can never be considered equal (in
        // terms of the equals() method) and hence they re not be related types.
        // If you need to convert between the types then separate conversion
        // methods should be introduced.

        private final S2Point a;
        private final S2Point b;

        public UndirectedEdge(S2Point start, S2Point end) {
            this.a = start;
            this.b = end;
        }

        public S2Point getStart() {
            return a;
        }

        public S2Point getEnd() {
            return b;
        }

        @Override
        public String toString() {
            return String.format("Edge: (%s <-> %s)\n   or [%s <-> %s]", a.toDegreesString(), b.toDegreesString(),
                    a, b);
        }

        @Override
        public boolean equals(Object o) {
            if (o == null || !(o instanceof UndirectedEdge)) {
                return false;
            }
            UndirectedEdge other = (UndirectedEdge) o;
            return ((getStart().equals(other.getStart()) && getEnd().equals(other.getEnd()))
                    || (getStart().equals(other.getEnd()) && getEnd().equals(other.getStart())));
        }

        @Override
        public int hashCode() {
            return getStart().hashCode() + getEnd().hashCode();
        }
    }

    private static final class LoopVertexIndexPair {
        private final int loopIndex;
        private final int vertexIndex;

        public LoopVertexIndexPair(int loopIndex, int vertexIndex) {
            this.loopIndex = loopIndex;
            this.vertexIndex = vertexIndex;
        }

        public int getLoopIndex() {
            return loopIndex;
        }

        public int getVertexIndex() {
            return vertexIndex;
        }
    }

    /**
     * An S2Point that also has a parameter associated with it, which corresponds
     * to a time-like order on the points.
     */
    private static final class ParametrizedS2Point implements Comparable<ParametrizedS2Point> {
        private final double time;
        private final S2Point point;

        public ParametrizedS2Point(double time, S2Point point) {
            this.time = time;
            this.point = point;
        }

        public double getTime() {
            return time;
        }

        public S2Point getPoint() {
            return point;
        }

        @Override
        public int compareTo(ParametrizedS2Point o) {
            int compareTime = Double.compare(time, o.time);
            if (compareTime != 0) {
                return compareTime;
            }
            return point.compareTo(o.point);
        }
    }
}