• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2006 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.geometry;
18 
19 import com.google.common.collect.ForwardingMultimap;
20 import com.google.common.collect.HashMultimap;
21 import com.google.common.collect.HashMultiset;
22 import com.google.common.collect.Lists;
23 import com.google.common.collect.Maps;
24 import com.google.common.collect.Multimap;
25 import com.google.common.collect.Multiset;
26 
27 import java.util.Collection;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Stack;
31 import java.util.logging.Logger;
32 
33 /**
34  * This is a simple class for assembling polygons out of edges. It requires that
35  * no two edges cross. It can handle both directed and undirected edges, and
36  * optionally it can also remove duplicate edge pairs (consisting of two
37  * identical edges or an edge and its reverse edge). This is useful for
38  * computing seamless unions of polygons that have been cut into pieces.
39  *
40  *  Here are some of the situations this class was designed to handle:
41  *
42  *  1. Computing the union of disjoint polygons that may share part of their
43  * boundaries. For example, reassembling a lake that has been split into two
44  * loops by a state boundary.
45  *
46  *  2. Constructing polygons from input data that does not follow S2
47  * conventions, i.e. where loops may have repeated vertices, or distinct loops
48  * may share edges, or shells and holes have opposite or unspecified
49  * orientations.
50  *
51  *  3. Computing the symmetric difference of a set of polygons whose edges
52  * intersect only at vertices. This can be used to implement a limited form of
53  * polygon intersection or subtraction as well as unions.
54  *
55  *  4. As a tool for implementing other polygon operations by generating a
56  * collection of directed edges and then assembling them into loops.
57  *
58  */
59 public strictfp class S2PolygonBuilder {
60   private static final Logger log = Logger.getLogger(S2PolygonBuilder.class.getCanonicalName());
61 
62   private Options options;
63 
64   /**
65    * The current set of edges, grouped by origin. The set of destination
66    * vertices is a multiset so that the same edge can be present more than once.
67    */
68   private Map<S2Point, Multiset<S2Point>> edges;
69 
70   /**
71    * Default constructor for well-behaved polygons. Uses the DIRECTED_XOR
72    * options.
73    */
S2PolygonBuilder()74   public S2PolygonBuilder() {
75     this(Options.DIRECTED_XOR);
76 
77   }
78 
S2PolygonBuilder(Options options)79   public S2PolygonBuilder(Options options) {
80     this.options = options;
81     this.edges = Maps.newHashMap();
82   }
83 
84   public enum Options {
85 
86     /**
87      * These are the options that should be used for assembling well-behaved
88      * input data into polygons. All edges should be directed such that "shells"
89      * and "holes" have opposite orientations (typically CCW shells and
90      * clockwise holes), unless it is known that shells and holes do not share
91      * any edges.
92      */
93     DIRECTED_XOR(false, true),
94 
95     /**
96      * These are the options that should be used for assembling polygons that do
97      * not follow the conventions above, e.g. where edge directions may vary
98      * within a single loop, or shells and holes are not oppositely oriented.
99      */
100     UNDIRECTED_XOR(true, true),
101 
102     /**
103      * These are the options that should be used for assembling edges where the
104      * desired output is a collection of loops rather than a polygon, and edges
105      * may occur more than once. Edges are treated as undirected and are not
106      * XORed together, in particular, adding edge A->B also adds B->A.
107      */
108     UNDIRECTED_UNION(true, false),
109 
110     /**
111      * Finally, select this option when the desired output is a collection of
112      * loops rather than a polygon, but your input edges are directed and you do
113      * not want reverse edges to be added implicitly as above.
114      */
115     DIRECTED_UNION(false, false);
116 
117     private boolean undirectedEdges;
118     private boolean xorEdges;
119     private boolean validate;
120     private S1Angle mergeDistance;
121 
Options(boolean undirectedEdges, boolean xorEdges)122     private Options(boolean undirectedEdges, boolean xorEdges) {
123       this.undirectedEdges = undirectedEdges;
124       this.xorEdges = xorEdges;
125       this.validate = false;
126       this.mergeDistance = S1Angle.radians(0);
127     }
128 
129     /**
130      * If "undirected_edges" is false, then the input is assumed to consist of
131      * edges that can be assembled into oriented loops without reversing any of
132      * the edges. Otherwise, "undirected_edges" should be set to true.
133      */
getUndirectedEdges()134     public boolean getUndirectedEdges() {
135       return undirectedEdges;
136     }
137 
138     /**
139      * If "xor_edges" is true, then any duplicate edge pairs are removed. This
140      * is useful for computing the union of a collection of polygons whose
141      * interiors are disjoint but whose boundaries may share some common edges
142      * (e.g. computing the union of South Africa, Lesotho, and Swaziland).
143      *
144      *  Note that for directed edges, a "duplicate edge pair" consists of an
145      * edge and its corresponding reverse edge. This means that either (a)
146      * "shells" and "holes" must have opposite orientations, or (b) shells and
147      * holes do not share edges. Otherwise undirected_edges() should be
148      * specified.
149      *
150      *  There are only two reasons to turn off xor_edges():
151      *
152      *  (1) assemblePolygon() will be called, and you want to assert that there
153      * are no duplicate edge pairs in the input.
154      *
155      *  (2) assembleLoops() will be called, and you want to keep abutting loops
156      * separate in the output rather than merging their regions together (e.g.
157      * assembling loops for Kansas City, KS and Kansas City, MO simultaneously).
158      */
getXorEdges()159     public boolean getXorEdges() {
160       return xorEdges;
161     }
162 
163     /**
164      * Default value: false
165      */
getValidate()166     public boolean getValidate() {
167       return validate;
168     }
169 
170     /**
171      * Default value: 0
172      */
getMergeDistance()173     public S1Angle getMergeDistance() {
174       return mergeDistance;
175     }
176 
177     /**
178      * If true, isValid() is called on all loops and polygons before
179      * constructing them. If any loop is invalid (e.g. self-intersecting), it is
180      * rejected and returned as a set of "unused edges". Any remaining valid
181      * loops are kept. If the entire polygon is invalid (e.g. two loops
182      * intersect), then all loops are rejected and returned as unused edges.
183      */
setValidate(boolean validate)184     public void setValidate(boolean validate) {
185       this.validate = validate;
186     }
187 
188     /**
189      * If set to a positive value, all vertices that are separated by at most
190      * this distance will be merged together. In addition, vertices that are
191      * closer than this distance to a non-incident edge will be spliced into it
192      * (TODO).
193      *
194      *  The merging is done in such a way that all vertex-vertex and vertex-edge
195      * distances in the output are greater than 'merge_distance'.
196      *
197      *  This method is useful for assembling polygons out of input data where
198      * vertices and/or edges may not be perfectly aligned.
199      */
setMergeDistance(S1Angle mergeDistance)200     public void setMergeDistance(S1Angle mergeDistance) {
201       this.mergeDistance = mergeDistance;
202     }
203 
204     // Used for testing only
setUndirectedEdges(boolean undirectedEdges)205     void setUndirectedEdges(boolean undirectedEdges) {
206       this.undirectedEdges = undirectedEdges;
207     }
208 
209     // Used for testing only
setXorEdges(boolean xorEdges)210     void setXorEdges(boolean xorEdges) {
211       this.xorEdges = xorEdges;
212     }
213   }
214 
options()215   public Options options() {
216     return options;
217   }
218 
219   /**
220    * Add the given edge to the polygon builder. This method should be used for
221    * input data that may not follow S2 polygon conventions. Note that edges are
222    * not allowed to cross each other. Also note that as a convenience, edges
223    * where v0 == v1 are ignored.
224    */
addEdge(S2Point v0, S2Point v1)225   public void addEdge(S2Point v0, S2Point v1) {
226     // If xor_edges is true, we look for an existing edge in the opposite
227     // direction. We either delete that edge or insert a new one.
228 
229     if (v0.equals(v1)) {
230       return;
231     }
232 
233     if (options.getXorEdges()) {
234       Multiset<S2Point> candidates = edges.get(v1);
235       if (candidates != null && candidates.count(v0) > 0) {
236         eraseEdge(v1, v0);
237         return;
238       }
239     }
240 
241     if (edges.get(v0) == null) {
242       edges.put(v0, HashMultiset.<S2Point>create());
243     }
244 
245     edges.get(v0).add(v1);
246     if (options.getUndirectedEdges()) {
247       if (edges.get(v1) == null) {
248         edges.put(v1, HashMultiset.<S2Point>create());
249       }
250       edges.get(v1).add(v0);
251     }
252   }
253 
254   /**
255    * Add all edges in the given loop. If the sign() of the loop is negative
256    * (i.e. this loop represents a hole), the reverse edges are added instead.
257    * This implies that "shells" are CCW and "holes" are CW, as required for the
258    * directed edges convention described above.
259    *
260    * This method does not take ownership of the loop.
261    */
addLoop(S2Loop loop)262   public void addLoop(S2Loop loop) {
263     int sign = loop.sign();
264     for (int i = loop.numVertices(); i > 0; --i) {
265       // Vertex indices need to be in the range [0, 2*num_vertices()-1].
266       addEdge(loop.vertex(i), loop.vertex(i + sign));
267     }
268   }
269 
270   /**
271    * Add all loops in the given polygon. Shells and holes are added with
272    * opposite orientations as described for AddLoop(). This method does not take
273    * ownership of the polygon.
274    */
addPolygon(S2Polygon polygon)275   public void addPolygon(S2Polygon polygon) {
276     for (int i = 0; i < polygon.numLoops(); ++i) {
277       addLoop(polygon.loop(i));
278     }
279   }
280 
281   /**
282    * Assembles the given edges into as many non-crossing loops as possible. When
283    * there is a choice about how to assemble the loops, then CCW loops are
284    * preferred. Returns true if all edges were assembled. If "unused_edges" is
285    * not NULL, it is initialized to the set of edges that could not be assembled
286    * into loops.
287    *
288    *  Note that if xor_edges() is false and duplicate edge pairs may be present,
289    * then undirected_edges() should be specified unless all loops can be
290    * assembled in a counter-clockwise direction. Otherwise this method may not
291    * be able to assemble all loops due to its preference for CCW loops.
292    *
293    * This method resets the S2PolygonBuilder state so that it can be reused.
294    */
assembleLoops(List<S2Loop> loops, List<S2Edge> unusedEdges)295   public boolean assembleLoops(List<S2Loop> loops, List<S2Edge> unusedEdges) {
296     if (options.getMergeDistance().radians() > 0) {
297       mergeVertices();
298     }
299 
300     List<S2Edge> dummyUnusedEdges = Lists.newArrayList();
301     if (unusedEdges == null) {
302       unusedEdges = dummyUnusedEdges;
303     }
304 
305     // We repeatedly choose an arbitrary edge and attempt to assemble a loop
306     // starting from that edge. (This is always possible unless the input
307     // includes extra edges that are not part of any loop.)
308 
309     unusedEdges.clear();
310     while (!edges.isEmpty()) {
311       Map.Entry<S2Point, Multiset<S2Point>> edge = edges.entrySet().iterator().next();
312 
313       S2Point v0 = edge.getKey();
314       S2Point v1 = edge.getValue().iterator().next();
315 
316       S2Loop loop = assembleLoop(v0, v1, unusedEdges);
317       if (loop == null) {
318         continue;
319       }
320 
321       // In the case of undirected edges, we may have assembled a clockwise
322       // loop while trying to assemble a CCW loop. To fix this, we assemble
323       // a new loop starting with an arbitrary edge in the reverse direction.
324       // This is guaranteed to assemble a loop that is interior to the previous
325       // one and will therefore eventually terminate.
326 
327       while (options.getUndirectedEdges() && !loop.isNormalized()) {
328         loop = assembleLoop(loop.vertex(1), loop.vertex(0), unusedEdges);
329       }
330       loops.add(loop);
331       eraseLoop(loop, loop.numVertices());
332     }
333     return unusedEdges.isEmpty();
334   }
335 
336   /**
337    * Like AssembleLoops, but normalizes all the loops so that they enclose less
338    * than half the sphere, and then assembles the loops into a polygon.
339    *
340    *  For this method to succeed, there should be no duplicate edges in the
341    * input. If this is not known to be true, then the "xor_edges" option should
342    * be set (which is true by default).
343    *
344    *  Note that S2Polygons cannot represent arbitrary regions on the sphere,
345    * because of the limitation that no loop encloses more than half of the
346    * sphere. For example, an S2Polygon cannot represent a 100km wide band around
347    * the equator. In such cases, this method will return the *complement* of the
348    * expected region. So for example if all the world's coastlines were
349    * assembled, the output S2Polygon would represent the land area (irrespective
350    * of the input edge or loop orientations).
351    */
assemblePolygon(S2Polygon polygon, List<S2Edge> unusedEdges)352   public boolean assemblePolygon(S2Polygon polygon, List<S2Edge> unusedEdges) {
353     List<S2Loop> loops = Lists.newArrayList();
354     boolean success = assembleLoops(loops, unusedEdges);
355 
356     // If edges are undirected, then all loops are already CCW. Otherwise we
357     // need to make sure the loops are normalized.
358     if (!options.getUndirectedEdges()) {
359       for (int i = 0; i < loops.size(); ++i) {
360         loops.get(i).normalize();
361       }
362     }
363     if (options.getValidate() && !S2Polygon.isValid(loops)) {
364       if (unusedEdges != null) {
365         for (S2Loop loop : loops) {
366           rejectLoop(loop, loop.numVertices(), unusedEdges);
367         }
368       }
369       return false;
370     }
371     polygon.init(loops);
372     return success;
373   }
374 
375   /**
376    * Convenience method for when you don't care about unused edges.
377    */
assemblePolygon()378   public S2Polygon assemblePolygon() {
379     S2Polygon polygon = new S2Polygon();
380     List<S2Edge> unusedEdges = Lists.newArrayList();
381 
382     assemblePolygon(polygon, unusedEdges);
383 
384     return polygon;
385   }
386 
387   // Debugging functions:
388 
dumpEdges(S2Point v0)389   protected void dumpEdges(S2Point v0) {
390     log.info(v0.toString());
391     Multiset<S2Point> vset = edges.get(v0);
392     if (vset != null) {
393       for (S2Point v : vset) {
394         log.info("    " + v.toString());
395       }
396     }
397   }
398 
dump()399   protected void dump() {
400     for (S2Point v : edges.keySet()) {
401       dumpEdges(v);
402     }
403   }
404 
eraseEdge(S2Point v0, S2Point v1)405   private void eraseEdge(S2Point v0, S2Point v1) {
406     // Note that there may be more than one copy of an edge if we are not XORing
407     // them, so a VertexSet is a multiset.
408 
409     Multiset<S2Point> vset = edges.get(v0);
410     // assert (vset.count(v1) > 0);
411     vset.remove(v1);
412     if (vset.isEmpty()) {
413       edges.remove(v0);
414     }
415 
416     if (options.getUndirectedEdges()) {
417       vset = edges.get(v1);
418       // assert (vset.count(v0) > 0);
419       vset.remove(v0);
420       if (vset.isEmpty()) {
421         edges.remove(v1);
422       }
423     }
424   }
425 
eraseLoop(List<S2Point> v, int n)426   private void eraseLoop(List<S2Point> v, int n) {
427     for (int i = n - 1, j = 0; j < n; i = j++) {
428       eraseEdge(v.get(i), v.get(j));
429     }
430   }
431 
eraseLoop(S2Loop v, int n)432   private void eraseLoop(S2Loop v, int n) {
433     for (int i = n - 1, j = 0; j < n; i = j++) {
434       eraseEdge(v.vertex(i), v.vertex(j));
435     }
436   }
437 
438   /**
439    * We start at the given edge and assemble a loop taking left turns whenever
440    * possible. We stop the loop as soon as we encounter any vertex that we have
441    * seen before *except* for the first vertex (v0). This ensures that only CCW
442    * loops are constructed when possible.
443    */
assembleLoop(S2Point v0, S2Point v1, List<S2Edge> unusedEdges)444   private S2Loop assembleLoop(S2Point v0, S2Point v1, List<S2Edge> unusedEdges) {
445 
446     // The path so far.
447     List<S2Point> path = Lists.newArrayList();
448 
449     // Maps a vertex to its index in "path".
450     Map<S2Point, Integer> index = Maps.newHashMap();
451     path.add(v0);
452     path.add(v1);
453 
454     index.put(v1, 1);
455 
456     while (path.size() >= 2) {
457       // Note that "v0" and "v1" become invalid if "path" is modified.
458       v0 = path.get(path.size() - 2);
459       v1 = path.get(path.size() - 1);
460 
461       S2Point v2 = null;
462       boolean v2Found = false;
463       Multiset<S2Point> vset = edges.get(v1);
464       if (vset != null) {
465         for (S2Point v : vset) {
466           // We prefer the leftmost outgoing edge, ignoring any reverse edges.
467           if (v.equals(v0)) {
468             continue;
469           }
470           if (!v2Found || S2.orderedCCW(v0, v2, v, v1)) {
471             v2 = v;
472           }
473           v2Found = true;
474         }
475       }
476       if (!v2Found) {
477         // We've hit a dead end. Remove this edge and backtrack.
478         unusedEdges.add(new S2Edge(v0, v1));
479         eraseEdge(v0, v1);
480         index.remove(v1);
481         path.remove(path.size() - 1);
482       } else if (index.get(v2) == null) {
483         // This is the first time we've visited this vertex.
484         index.put(v2, path.size());
485         path.add(v2);
486       } else {
487         // We've completed a loop. Throw away any initial vertices that
488         // are not part of the loop.
489         path = path.subList(index.get(v2), path.size());
490 
491         if (options.getValidate() && !S2Loop.isValid(path)) {
492           // We've constructed a loop that crosses itself, which can only happen
493           // if there is bad input data. Throw away the whole loop.
494           rejectLoop(path, path.size(), unusedEdges);
495           eraseLoop(path, path.size());
496           return null;
497         }
498         return new S2Loop(path);
499       }
500     }
501     return null;
502   }
503 
504   /** Erases all edges of the given loop and marks them as unused. */
rejectLoop(S2Loop v, int n, List<S2Edge> unusedEdges)505   private void rejectLoop(S2Loop v, int n, List<S2Edge> unusedEdges) {
506     for (int i = n - 1, j = 0; j < n; i = j++) {
507       unusedEdges.add(new S2Edge(v.vertex(i), v.vertex(j)));
508     }
509   }
510 
511   /** Erases all edges of the given loop and marks them as unused. */
rejectLoop(List<S2Point> v, int n, List<S2Edge> unusedEdges)512   private void rejectLoop(List<S2Point> v, int n, List<S2Edge> unusedEdges) {
513     for (int i = n - 1, j = 0; j < n; i = j++) {
514       unusedEdges.add(new S2Edge(v.get(i), v.get(j)));
515     }
516   }
517 
518   /** Moves a set of vertices from old to new positions. */
moveVertices(Map<S2Point, S2Point> mergeMap)519   private void moveVertices(Map<S2Point, S2Point> mergeMap) {
520     if (mergeMap.isEmpty()) {
521       return;
522     }
523 
524     // We need to copy the set of edges affected by the move, since
525     // this.edges_could be reallocated when we start modifying it.
526     List<S2Edge> edgesCopy = Lists.newArrayList();
527     for (Map.Entry<S2Point, Multiset<S2Point>> edge : this.edges.entrySet()) {
528       S2Point v0 = edge.getKey();
529       Multiset<S2Point> vset = edge.getValue();
530       for (S2Point v1 : vset) {
531         if (mergeMap.get(v0) != null || mergeMap.get(v1) != null) {
532 
533           // We only need to modify one copy of each undirected edge.
534           if (!options.getUndirectedEdges() || v0.lessThan(v1)) {
535             edgesCopy.add(new S2Edge(v0, v1));
536           }
537         }
538       }
539     }
540 
541     // Now erase all the old edges, and add all the new edges. This will
542     // automatically take care of any XORing that needs to be done, because
543     // EraseEdge also erases the sibiling of undirected edges.
544     for (int i = 0; i < edgesCopy.size(); ++i) {
545       S2Point v0 = edgesCopy.get(i).getStart();
546       S2Point v1 = edgesCopy.get(i).getEnd();
547       eraseEdge(v0, v1);
548       if (mergeMap.get(v0) != null) {
549         v0 = mergeMap.get(v0);
550       }
551       if (mergeMap.get(v1) != null) {
552         v1 = mergeMap.get(v1);
553       }
554       addEdge(v0, v1);
555     }
556   }
557 
558   /**
559    * Look for groups of vertices that are separated by at most merge_distance()
560    * and merge them into a single vertex.
561    */
mergeVertices()562   private void mergeVertices() {
563     // The overall strategy is to start from each vertex and grow a maximal
564     // cluster of mergable vertices. In graph theoretic terms, we find the
565     // connected components of the undirected graph whose edges connect pairs of
566     // vertices that are separated by at most merge_distance.
567     //
568     // We then choose a single representative vertex for each cluster, and
569     // update all the edges appropriately. We choose an arbitrary existing
570     // vertex rather than computing the centroid of all the vertices to avoid
571     // creating new vertex pairs that need to be merged. (We guarantee that all
572     // vertex pairs are separated by at least merge_distance in the output.)
573 
574     PointIndex index = new PointIndex(options.getMergeDistance().radians());
575 
576     for (Map.Entry<S2Point, Multiset<S2Point>> edge : edges.entrySet()) {
577       index.add(edge.getKey());
578       Multiset<S2Point> vset = edge.getValue();
579       for (S2Point v : vset) {
580         index.add(v);
581       }
582     }
583 
584     // Next, we loop through all the vertices and attempt to grow a maximial
585     // mergeable group starting from each vertex.
586 
587     Map<S2Point, S2Point> mergeMap = Maps.newHashMap();
588     Stack<S2Point> frontier = new Stack<S2Point>();
589     List<S2Point> mergeable = Lists.newArrayList();
590 
591     for (Map.Entry<S2CellId, MarkedS2Point> entry : index.entries()) {
592       MarkedS2Point point = entry.getValue();
593       if (point.isMarked()) {
594         continue; // Already processed.
595       }
596 
597       point.mark();
598 
599       // Grow a maximal mergeable component starting from "vstart", the
600       // canonical representative of the mergeable group.
601       S2Point vstart = point.getPoint();
602       frontier.push(vstart);
603       while (!frontier.isEmpty()) {
604         S2Point v0 = frontier.pop();
605 
606         index.query(v0, mergeable);
607         for (S2Point v1 : mergeable) {
608           frontier.push(v1);
609           mergeMap.put(v1, vstart);
610         }
611       }
612     }
613 
614     // Finally, we need to replace vertices according to the merge_map.
615     moveVertices(mergeMap);
616   }
617 
618   /**
619    * A PointIndex is a cheap spatial index to help us find mergeable vertices.
620    * Given a set of points, it can efficiently find all of the points within a
621    * given search radius of an arbitrary query location. It is essentially just
622    * a hash map from cell ids at a given fixed level to the set of points
623    * contained by that cell id.
624    *
625    *  This class is not suitable for general use because it only supports
626    * fixed-radius queries and has various special-purpose operations to avoid
627    * the need for additional data structures.
628    */
629   private class PointIndex extends ForwardingMultimap<S2CellId, MarkedS2Point> {
630     private double searchRadius;
631     private int level;
632     private final Multimap<S2CellId, MarkedS2Point> delegate = HashMultimap.create();
633 
PointIndex(double searchRadius)634     public PointIndex(double searchRadius) {
635 
636       this.searchRadius = searchRadius;
637 
638       // We choose a cell level such that if dist(A,B) <= search_radius, the
639       // S2CellId at that level containing A is a vertex neighbor of B (see
640       // S2CellId.getVertexNeighbors). This turns out to be the highest
641       // level such that a spherical cap (i.e. "disc") of the given radius
642       // fits completely inside all cells at that level.
643       this.level =
644           Math.min(S2Projections.MIN_WIDTH.getMaxLevel(2 * searchRadius), S2CellId.MAX_LEVEL - 1);
645     }
646 
647     @Override
delegate()648     protected Multimap<S2CellId, MarkedS2Point> delegate() {
649       return delegate;
650     }
651 
652     /** Add a point to the index if it does not already exist. */
add(S2Point p)653     public void add(S2Point p) {
654       S2CellId id = S2CellId.fromPoint(p).parent(level);
655       Collection<MarkedS2Point> pointSet = get(id);
656       for (MarkedS2Point point : pointSet) {
657         if (point.getPoint().equals(p)) {
658           return;
659         }
660       }
661       put(id, new MarkedS2Point(p));
662     }
663 
664     /**
665      * Return the set the unmarked points whose distance to "center" is less
666      * than search_radius_, and mark these points. By construction, these points
667      * will be contained by one of the vertex neighbors of "center".
668      */
query(S2Point center, List<S2Point> output)669     public void query(S2Point center, List<S2Point> output) {
670       output.clear();
671 
672       List<S2CellId> neighbors = Lists.newArrayList();
673       S2CellId.fromPoint(center).getVertexNeighbors(level, neighbors);
674       for (S2CellId id : neighbors) {
675         // Iterate over the points contained by each vertex neighbor.
676         for (MarkedS2Point mp : get(id)) {
677           if (mp.isMarked()) {
678             continue;
679           }
680           S2Point p = mp.getPoint();
681 
682           if (center.angle(p) <= searchRadius) {
683             output.add(p);
684             mp.mark();
685           }
686         }
687       }
688     }
689   }
690 
691   /**
692    * An S2Point that can be marked. Used in PointIndex.
693    */
694   private class MarkedS2Point {
695     private S2Point point;
696     private boolean mark;
697 
MarkedS2Point(S2Point point)698     public MarkedS2Point(S2Point point) {
699       this.point = point;
700       this.mark = false;
701     }
702 
isMarked()703     public boolean isMarked() {
704       return mark;
705     }
706 
getPoint()707     public S2Point getPoint() {
708       return point;
709     }
710 
mark()711     public void mark() {
712       // assert (!isMarked());
713       this.mark = true;
714     }
715   }
716 }
717