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