* Copyright 2005 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 java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.PriorityQueue;
* An S2RegionCoverer is a class that allows arbitrary regions to be
* approximated as unions of cells (S2CellUnion). This is useful for
* implementing various sorts of search and precomputation operations.
*
* Typical usage: {@code S2RegionCoverer coverer; coverer.setMaxCells(5); S2Cap
* cap = S2Cap.fromAxisAngle(...); S2CellUnion covering;
* coverer.getCovering(cap, covering); * }
*
* This yields a cell union of at most 5 cells that is guaranteed to cover the
* given cap (a disc-shaped region on the sphere).
*
* The approximation algorithm is not optimal but does a pretty good job in
* practice. The output does not always use the maximum number of cells allowed,
* both because this would not always yield a better approximation, and because
* max_cells() is a limit on how much work is done exploring the possible
* covering as well as a limit on the final output size.
*
* One can also generate interior coverings, which are sets of cells which are
* entirely contained within a region. Interior coverings can be empty, even for
* non-empty regions, if there are no cells that satisfy the provided
* constraints and are contained by the region. Note that for performance
* reasons, it is wise to specify a max_level when computing interior coverings
* - otherwise for regions with small or zero area, the algorithm may spend a
* lot of time subdividing cells all the way to leaf level to try to find
* contained cells.
*
* This class is thread-unsafe. Simultaneous calls to any of the getCovering
* methods will conflict and produce unpredictable results.
*
*/
public final strictfp class S2RegionCoverer {
* By default, the covering uses at most 8 cells at any level. This gives a
* reasonable tradeoff between the number of cells used and the accuracy of
* the approximation (see table below).
*/
public static final int DEFAULT_MAX_CELLS = 8;
private static final S2Cell[] FACE_CELLS = new S2Cell[6];
static {
for (int face = 0; face < 6; ++face) {
FACE_CELLS[face] = S2Cell.fromFacePosLevel(face, (byte) 0, 0);
}
}
private int minLevel;
private int maxLevel;
private int levelMod;
private int maxCells;
private boolean interiorCovering;
private int candidatesCreatedCounter;
* We save a temporary copy of the pointer passed to GetCovering() in order to
* avoid passing this parameter around internally. It is only used (and only
* valid) for the duration of a single GetCovering() call.
*/
S2Region region;
* A temporary variable used by GetCovering() that holds the cell ids that
* have been added to the covering so far.
*/
ArrayList<S2CellId> result;
static class Candidate {
private S2Cell cell;
private boolean isTerminal;
private int numChildren;
private Candidate[] children;
}
static class QueueEntry {
private int id;
private Candidate candidate;
public QueueEntry(int id, Candidate candidate) {
this.id = id;
this.candidate = candidate;
}
}
* We define our own comparison function on QueueEntries in order to make the
* results deterministic. Using the default less<QueueEntry>, entries of equal
* priority would be sorted according to the memory address of the candidate.
*/
static class QueueEntriesComparator implements Comparator<QueueEntry> {
@Override
public int compare(S2RegionCoverer.QueueEntry x, S2RegionCoverer.QueueEntry y) {
return x.id < y.id ? 1 : (x.id > y.id ? -1 : 0);
}
}
* We keep the candidates in a priority queue. We specify a vector to hold the
* queue entries since for some reason priority_queue<> uses a deque by
* default.
*/
private PriorityQueue<QueueEntry> candidateQueue;
* Default constructor, sets all fields to default values.
*/
public S2RegionCoverer() {
minLevel = 0;
maxLevel = S2CellId.MAX_LEVEL;
levelMod = 1;
maxCells = DEFAULT_MAX_CELLS;
this.region = null;
result = new ArrayList<S2CellId>();
candidateQueue = new PriorityQueue<QueueEntry>(10, new QueueEntriesComparator());
}
* Sets the minimum level to be used.
*/
public void setMinLevel(int minLevel) {
this.minLevel = Math.max(0, Math.min(S2CellId.MAX_LEVEL, minLevel));
}
* Sets the maximum level to be used.
*/
public void setMaxLevel(int maxLevel) {
this.maxLevel = Math.max(0, Math.min(S2CellId.MAX_LEVEL, maxLevel));
}
public int minLevel() {
return minLevel;
}
public int maxLevel() {
return maxLevel;
}
public int maxCells() {
return maxCells;
}
* If specified, then only cells where (level - min_level) is a multiple of
* "level_mod" will be used (default 1). This effectively allows the branching
* factor of the S2CellId hierarchy to be increased. Currently the only
* parameter values allowed are 1, 2, or 3, corresponding to branching factors
* of 4, 16, and 64 respectively.
*/
public void setLevelMod(int levelMod) {
this.levelMod = Math.max(1, Math.min(3, levelMod));
}
public int levelMod() {
return levelMod;
}
* Sets the maximum desired number of cells in the approximation (defaults to
* kDefaultMaxCells). Note the following:
*
* <ul>
* <li>For any setting of max_cells(), up to 6 cells may be returned if that
* is the minimum number of cells required (e.g. if the region intersects all
* six face cells). Up to 3 cells may be returned even for very tiny convex
* regions if they happen to be located at the intersection of three cube
* faces.
*
* <li>For any setting of max_cells(), an arbitrary number of cells may be
* returned if min_level() is too high for the region being approximated.
*
* <li>If max_cells() is less than 4, the area of the covering may be
* arbitrarily large compared to the area of the original region even if the
* region is convex (e.g. an S2Cap or S2LatLngRect).
* </ul>
*
* Accuracy is measured by dividing the area of the covering by the area of
* the original region. The following table shows the median and worst case
* values for this area ratio on a test case consisting of 100,000 spherical
* caps of random size (generated using s2regioncoverer_unittest):
*
* <pre>
* max_cells: 3 4 5 6 8 12 20 100 1000
* median ratio: 5.33 3.32 2.73 2.34 1.98 1.66 1.42 1.11 1.01
* worst case: 215518 14.41 9.72 5.26 3.91 2.75 1.92 1.20 1.02
* </pre>
*/
public void setMaxCells(int maxCells) {
this.maxCells = maxCells;
}
* Computes a list of cell ids that covers the given region and satisfies the
* various restrictions specified above.
*
* @param region The region to cover
* @param covering The list filled in by this method
*/
public void getCovering(S2Region region, ArrayList<S2CellId> covering) {
S2CellUnion tmp = getCovering(region);
tmp.denormalize(minLevel(), levelMod(), covering);
}
* Computes a list of cell ids that is contained within the given region and
* satisfies the various restrictions specified above.
*
* @param region The region to fill
* @param interior The list filled in by this method
*/
public void getInteriorCovering(S2Region region, ArrayList<S2CellId> interior) {
S2CellUnion tmp = getInteriorCovering(region);
tmp.denormalize(minLevel(), levelMod(), interior);
}
* Return a normalized cell union that covers the given region and satisfies
* the restrictions *EXCEPT* for min_level() and level_mod(). These criteria
* cannot be satisfied using a cell union because cell unions are
* automatically normalized by replacing four child cells with their parent
* whenever possible. (Note that the list of cell ids passed to the cell union
* constructor does in fact satisfy all the given restrictions.)
*/
public S2CellUnion getCovering(S2Region region) {
S2CellUnion covering = new S2CellUnion();
getCovering(region, covering);
return covering;
}
public void getCovering(S2Region region, S2CellUnion covering) {
interiorCovering = false;
getCoveringInternal(region);
covering.initSwap(result);
}
* Return a normalized cell union that is contained within the given region
* and satisfies the restrictions *EXCEPT* for min_level() and level_mod().
*/
public S2CellUnion getInteriorCovering(S2Region region) {
S2CellUnion covering = new S2CellUnion();
getInteriorCovering(region, covering);
return covering;
}
public void getInteriorCovering(S2Region region, S2CellUnion covering) {
interiorCovering = true;
getCoveringInternal(region);
covering.initSwap(result);
}
* Given a connected region and a starting point, return a set of cells at the
* given level that cover the region.
*/
public static void getSimpleCovering(
S2Region region, S2Point start, int level, ArrayList<S2CellId> output) {
floodFill(region, S2CellId.fromPoint(start).parent(level), output);
}
* If the cell intersects the given region, return a new candidate with no
* children, otherwise return null. Also marks the candidate as "terminal" if
* it should not be expanded further.
*/
private Candidate newCandidate(S2Cell cell) {
if (!region.mayIntersect(cell)) {
return null;
}
boolean isTerminal = false;
if (cell.level() >= minLevel) {
if (interiorCovering) {
if (region.contains(cell)) {
isTerminal = true;
} else if (cell.level() + levelMod > maxLevel) {
return null;
}
} else {
if (cell.level() + levelMod > maxLevel || region.contains(cell)) {
isTerminal = true;
}
}
}
Candidate candidate = new Candidate();
candidate.cell = cell;
candidate.isTerminal = isTerminal;
if (!isTerminal) {
candidate.children = new Candidate[1 << maxChildrenShift()];
}
candidatesCreatedCounter++;
return candidate;
}
private int maxChildrenShift() {
return 2 * levelMod;
}
* Process a candidate by either adding it to the result list or expanding its
* children and inserting it into the priority queue. Passing an argument of
* NULL does nothing.
*/
private void addCandidate(Candidate candidate) {
if (candidate == null) {
return;
}
if (candidate.isTerminal) {
result.add(candidate.cell.id());
return;
}
int numLevels = (candidate.cell.level() < minLevel) ? 1 : levelMod;
int numTerminals = expandChildren(candidate, candidate.cell, numLevels);
if (candidate.numChildren == 0) {
} else if (!interiorCovering && numTerminals == 1 << maxChildrenShift()
&& candidate.cell.level() >= minLevel) {
candidate.isTerminal = true;
addCandidate(candidate);
} else {
int priority = -((((candidate.cell.level() << maxChildrenShift()) + candidate.numChildren)
<< maxChildrenShift()) + numTerminals);
candidateQueue.add(new QueueEntry(priority, candidate));
}
}
* Populate the children of "candidate" by expanding the given number of
* levels from the given cell. Returns the number of children that were marked
* "terminal".
*/
private int expandChildren(Candidate candidate, S2Cell cell, int numLevels) {
numLevels--;
S2Cell[] childCells = new S2Cell[4];
for (int i = 0; i < 4; ++i) {
childCells[i] = new S2Cell();
}
cell.subdivide(childCells);
int numTerminals = 0;
for (int i = 0; i < 4; ++i) {
if (numLevels > 0) {
if (region.mayIntersect(childCells[i])) {
numTerminals += expandChildren(candidate, childCells[i], numLevels);
}
continue;
}
Candidate child = newCandidate(childCells[i]);
if (child != null) {
candidate.children[candidate.numChildren++] = child;
if (child.isTerminal) {
++numTerminals;
}
}
}
return numTerminals;
}
private void getInitialCandidates() {
if (maxCells >= 4) {
S2Cap cap = region.getCapBound();
int level = Math.min(S2Projections.MIN_WIDTH.getMaxLevel(2 * cap.angle().radians()),
Math.min(maxLevel(), S2CellId.MAX_LEVEL - 1));
if (levelMod() > 1 && level > minLevel()) {
level -= (level - minLevel()) % levelMod();
}
if (level > 0) {
ArrayList<S2CellId> base = new ArrayList<S2CellId>(4);
S2CellId id = S2CellId.fromPoint(cap.axis());
id.getVertexNeighbors(level, base);
for (int i = 0; i < base.size(); ++i) {
addCandidate(newCandidate(new S2Cell(base.get(i))));
}
return;
}
}
for (int face = 0; face < 6; ++face) {
addCandidate(newCandidate(FACE_CELLS[face]));
}
}
private void getCoveringInternal(S2Region region) {
Preconditions.checkState(candidateQueue.isEmpty() && result.isEmpty());
this.region = region;
candidatesCreatedCounter = 0;
getInitialCandidates();
while (!candidateQueue.isEmpty() && (!interiorCovering || result.size() < maxCells)) {
Candidate candidate = candidateQueue.poll().candidate;
if (candidate.cell.level() < minLevel || candidate.numChildren == 1
|| result.size() + (interiorCovering ? 0 : candidateQueue.size()) + candidate.numChildren
<= maxCells) {
for (int i = 0; i < candidate.numChildren; ++i) {
addCandidate(candidate.children[i]);
}
} else if (interiorCovering) {
} else {
candidate.isTerminal = true;
addCandidate(candidate);
}
}
candidateQueue.clear();
this.region = null;
}
* Given a region and a starting cell, return the set of all the
* edge-connected cells at the same level that intersect "region". The output
* cells are returned in arbitrary order.
*/
private static void floodFill(S2Region region, S2CellId start, ArrayList<S2CellId> output) {
HashSet<S2CellId> all = new HashSet<S2CellId>();
ArrayList<S2CellId> frontier = new ArrayList<S2CellId>();
output.clear();
all.add(start);
frontier.add(start);
while (!frontier.isEmpty()) {
S2CellId id = frontier.get(frontier.size() - 1);
frontier.remove(frontier.size() - 1);
if (!region.mayIntersect(new S2Cell(id))) {
continue;
}
output.add(id);
S2CellId[] neighbors = new S2CellId[4];
id.getEdgeNeighbors(neighbors);
for (int edge = 0; edge < 4; ++edge) {
S2CellId nbr = neighbors[edge];
boolean hasNbr = all.contains(nbr);
if (!all.contains(nbr)) {
frontier.add(nbr);
all.add(nbr);
}
}
}
}
}