blob: a7e51473ee742173bf810713166269b65362bec1 [file] [log] [blame]
/*
* 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;
// True if we're computing an interior covering.
private boolean interiorCovering;
// Counter of number of candidates created, for performance evaluation.
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; // Cell should not be expanded further.
private int numChildren; // Number of children that intersect the region.
private Candidate[] children; // Actual size may be 0, 4, 16, or 64
// elements.
}
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>();
// TODO(kirilll?): 10 is a completely random number, work out a better
// estimate
candidateQueue = new PriorityQueue<QueueEntry>(10, new QueueEntriesComparator());
}
// Set the minimum and maximum cell level to be used. The default is to use
// all cell levels. Requires: max_level() >= min_level().
//
// To find the cell level corresponding to a given physical distance, use
// the S2Cell metrics defined in s2.h. For example, to find the cell
// level that corresponds to an average edge length of 10km, use:
//
// int level = S2::kAvgEdge.GetClosestLevel(
// geostore::S2Earth::KmToRadians(length_km));
//
// Note: min_level() takes priority over max_cells(), i.e. cells below the
// given level will never be used even if this causes a large number of
// cells to be returned.
/**
* Sets the minimum level to be used.
*/
public void setMinLevel(int minLevel) {
// assert (minLevel >= 0 && minLevel <= S2CellId.MAX_LEVEL);
this.minLevel = Math.max(0, Math.min(S2CellId.MAX_LEVEL, minLevel));
}
/**
* Sets the maximum level to be used.
*/
public void setMaxLevel(int maxLevel) {
// assert (maxLevel >= 0 && maxLevel <= S2CellId.MAX_LEVEL);
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) {
// assert (levelMod >= 1 && levelMod <= 3);
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) {
// Rather than just returning the raw list of cell ids generated by
// GetCoveringInternal(), we construct a cell union and then denormalize it.
// This has the effect of replacing four child cells with their parent
// whenever this does not violate the covering parameters specified
// (min_level, level_mod, etc). This strategy significantly reduces the
// number of cells returned in many cases, and it is cheap compared to
// computing the covering in the first place.
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;
}
/** Return the log base 2 of the maximum number of children of a 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;
}
// assert (candidate.numChildren == 0);
// Expand one level at a time until we hit min_level_ to ensure that
// we don't skip over it.
int numLevels = (candidate.cell.level() < minLevel) ? 1 : levelMod;
int numTerminals = expandChildren(candidate, candidate.cell, numLevels);
if (candidate.numChildren == 0) {
// Do nothing
} else if (!interiorCovering && numTerminals == 1 << maxChildrenShift()
&& candidate.cell.level() >= minLevel) {
// Optimization: add the parent cell rather than all of its children.
// We can't do this for interior coverings, since the children just
// intersect the region, but may not be contained by it - we need to
// subdivide them further.
candidate.isTerminal = true;
addCandidate(candidate);
} else {
// We negate the priority so that smaller absolute priorities are returned
// first. The heuristic is designed to refine the largest cells first,
// since those are where we have the largest potential gain. Among cells
// at the same level, we prefer the cells with the smallest number of
// intersecting children. Finally, we prefer cells that have the smallest
// number of children that cannot be refined any further.
int priority = -((((candidate.cell.level() << maxChildrenShift()) + candidate.numChildren)
<< maxChildrenShift()) + numTerminals);
candidateQueue.add(new QueueEntry(priority, candidate));
// logger.info("Push: " + candidate.cell.id() + " (" + priority + ") ");
}
}
/**
* 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;
}
/** Computes a set of initial candidates that cover the given region. */
private void getInitialCandidates() {
// Optimization: if at least 4 cells are desired (the normal case),
// start with a 4-cell covering of the region's bounding cap. This
// lets us skip quite a few levels of refinement when the region to
// be covered is relatively small.
if (maxCells >= 4) {
// Find the maximum level such that the bounding cap contains at most one
// cell vertex at that level.
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();
}
// We don't bother trying to optimize the level == 0 case, since more than
// four face cells may be required.
if (level > 0) {
// Find the leaf cell containing the cap axis, and determine which
// subcell of the parent cell contains it.
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;
}
}
// Default: start with all six cube faces.
for (int face = 0; face < 6; ++face) {
addCandidate(newCandidate(FACE_CELLS[face]));
}
}
/** Generates a covering and stores it in result. */
private void getCoveringInternal(S2Region region) {
// Strategy: Start with the 6 faces of the cube. Discard any
// that do not intersect the shape. Then repeatedly choose the
// largest cell that intersects the shape and subdivide it.
//
// result contains the cells that will be part of the output, while the
// priority queue contains cells that we may still subdivide further. Cells
// that are entirely contained within the region are immediately added to
// the output, while cells that do not intersect the region are immediately
// discarded.
// Therefore pq_ only contains cells that partially intersect the region.
// Candidates are prioritized first according to cell size (larger cells
// first), then by the number of intersecting children they have (fewest
// children first), and then by the number of fully contained children
// (fewest children first).
Preconditions.checkState(candidateQueue.isEmpty() && result.isEmpty());
this.region = region;
candidatesCreatedCounter = 0;
getInitialCandidates();
while (!candidateQueue.isEmpty() && (!interiorCovering || result.size() < maxCells)) {
Candidate candidate = candidateQueue.poll().candidate;
// logger.info("Pop: " + candidate.cell.id());
if (candidate.cell.level() < minLevel || candidate.numChildren == 1
|| result.size() + (interiorCovering ? 0 : candidateQueue.size()) + candidate.numChildren
<= maxCells) {
// Expand this candidate into its children.
for (int i = 0; i < candidate.numChildren; ++i) {
addCandidate(candidate.children[i]);
}
} else if (interiorCovering) {
// Do nothing
} 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);
}
}
}
}
}