blob: 54c85a7e9b6b73fb4b8c040f3a16a54a9d4106e7 [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 java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public strictfp class S2CellTest extends GeometryTestCase {
public static final boolean DEBUG_MODE = true;
public void testFaces() {
Map<S2Point, Integer> edgeCounts = new HashMap<S2Point, Integer>();
Map<S2Point, Integer> vertexCounts = new HashMap<S2Point, Integer>();
for (int face = 0; face < 6; ++face) {
S2CellId id = S2CellId.fromFacePosLevel(face, 0, 0);
S2Cell cell = new S2Cell(id);
assertEquals(cell.id(), id);
assertEquals(cell.face(), face);
assertEquals(cell.level(), 0);
// Top-level faces have alternating orientations to get RHS coordinates.
assertEquals(cell.orientation(), face & S2.SWAP_MASK);
assertTrue(!cell.isLeaf());
for (int k = 0; k < 4; ++k) {
if (edgeCounts.containsKey(cell.getEdgeRaw(k))) {
edgeCounts.put(cell.getEdgeRaw(k), edgeCounts.get(cell
.getEdgeRaw(k)) + 1);
} else {
edgeCounts.put(cell.getEdgeRaw(k), 1);
}
if (vertexCounts.containsKey(cell.getVertexRaw(k))) {
vertexCounts.put(cell.getVertexRaw(k), vertexCounts.get(cell
.getVertexRaw(k)) + 1);
} else {
vertexCounts.put(cell.getVertexRaw(k), 1);
}
assertDoubleNear(cell.getVertexRaw(k).dotProd(cell.getEdgeRaw(k)), 0);
assertDoubleNear(cell.getVertexRaw((k + 1) & 3).dotProd(
cell.getEdgeRaw(k)), 0);
assertDoubleNear(S2Point.normalize(
S2Point.crossProd(cell.getVertexRaw(k), cell
.getVertexRaw((k + 1) & 3))).dotProd(cell.getEdge(k)), 1.0);
}
}
// Check that edges have multiplicity 2 and vertices have multiplicity 3.
for (Integer i : edgeCounts.values()) {
assertEquals(i.intValue(), 2);
}
for (Integer i : vertexCounts.values()) {
assertEquals(i.intValue(), 3);
}
}
static class LevelStats {
double count;
double minArea, maxArea, avgArea;
double minWidth, maxWidth, avgWidth;
double minEdge, maxEdge, avgEdge, maxEdgeAspect;
double minDiag, maxDiag, avgDiag, maxDiagAspect;
double minAngleSpan, maxAngleSpan, avgAngleSpan;
double minApproxRatio, maxApproxRatio;
LevelStats() {
count = 0;
minArea = 100;
maxArea = 0;
avgArea = 0;
minWidth = 100;
maxWidth = 0;
avgWidth = 0;
minEdge = 100;
maxEdge = 0;
avgEdge = 0;
maxEdgeAspect = 0;
minDiag = 100;
maxDiag = 0;
avgDiag = 0;
maxDiagAspect = 0;
minAngleSpan = 100;
maxAngleSpan = 0;
avgAngleSpan = 0;
minApproxRatio = 100;
maxApproxRatio = 0;
}
}
static List<LevelStats> levelStats = new ArrayList<LevelStats>(
S2CellId.MAX_LEVEL + 1);
static {
for (int i = 0; i < S2CellId.MAX_LEVEL + 1; ++i) {
levelStats.add(new LevelStats());
}
}
static void gatherStats(S2Cell cell) {
LevelStats s = levelStats.get(cell.level());
double exactArea = cell.exactArea();
double approxArea = cell.approxArea();
double minEdge = 100, maxEdge = 0, avgEdge = 0;
double minDiag = 100, maxDiag = 0;
double minWidth = 100, maxWidth = 0;
double minAngleSpan = 100, maxAngleSpan = 0;
for (int i = 0; i < 4; ++i) {
double edge = cell.getVertexRaw(i).angle(cell.getVertexRaw((i + 1) & 3));
minEdge = Math.min(edge, minEdge);
maxEdge = Math.max(edge, maxEdge);
avgEdge += 0.25 * edge;
S2Point mid = S2Point.add(cell.getVertexRaw(i), cell
.getVertexRaw((i + 1) & 3));
double width = S2.M_PI_2 - mid.angle(cell.getEdgeRaw(i ^ 2));
minWidth = Math.min(width, minWidth);
maxWidth = Math.max(width, maxWidth);
if (i < 2) {
double diag = cell.getVertexRaw(i).angle(cell.getVertexRaw(i ^ 2));
minDiag = Math.min(diag, minDiag);
maxDiag = Math.max(diag, maxDiag);
double angleSpan = cell.getEdgeRaw(i).angle(
S2Point.neg(cell.getEdgeRaw(i ^ 2)));
minAngleSpan = Math.min(angleSpan, minAngleSpan);
maxAngleSpan = Math.max(angleSpan, maxAngleSpan);
}
}
s.count += 1;
s.minArea = Math.min(exactArea, s.minArea);
s.maxArea = Math.max(exactArea, s.maxArea);
s.avgArea += exactArea;
s.minWidth = Math.min(minWidth, s.minWidth);
s.maxWidth = Math.max(maxWidth, s.maxWidth);
s.avgWidth += 0.5 * (minWidth + maxWidth);
s.minEdge = Math.min(minEdge, s.minEdge);
s.maxEdge = Math.max(maxEdge, s.maxEdge);
s.avgEdge += avgEdge;
s.maxEdgeAspect = Math.max(maxEdge / minEdge, s.maxEdgeAspect);
s.minDiag = Math.min(minDiag, s.minDiag);
s.maxDiag = Math.max(maxDiag, s.maxDiag);
s.avgDiag += 0.5 * (minDiag + maxDiag);
s.maxDiagAspect = Math.max(maxDiag / minDiag, s.maxDiagAspect);
s.minAngleSpan = Math.min(minAngleSpan, s.minAngleSpan);
s.maxAngleSpan = Math.max(maxAngleSpan, s.maxAngleSpan);
s.avgAngleSpan += 0.5 * (minAngleSpan + maxAngleSpan);
double approxRatio = approxArea / exactArea;
s.minApproxRatio = Math.min(approxRatio, s.minApproxRatio);
s.maxApproxRatio = Math.max(approxRatio, s.maxApproxRatio);
}
public void testSubdivide(S2Cell cell) {
gatherStats(cell);
if (cell.isLeaf()) {
return;
}
S2Cell[] children = new S2Cell[4];
for (int i = 0; i < children.length; ++i) {
children[i] = new S2Cell();
}
assertTrue(cell.subdivide(children));
S2CellId childId = cell.id().childBegin();
double exactArea = 0;
double approxArea = 0;
double averageArea = 0;
for (int i = 0; i < 4; ++i, childId = childId.next()) {
exactArea += children[i].exactArea();
approxArea += children[i].approxArea();
averageArea += children[i].averageArea();
// Check that the child geometry is consistent with its cell id.
assertEquals(children[i].id(), childId);
assertTrue(children[i].getCenter().aequal(childId.toPoint(), 1e-15));
S2Cell direct = new S2Cell(childId);
assertEquals(children[i].face(), direct.face());
assertEquals(children[i].level(), direct.level());
assertEquals(children[i].orientation(), direct.orientation());
assertEquals(children[i].getCenterRaw(), direct.getCenterRaw());
for (int k = 0; k < 4; ++k) {
assertEquals(children[i].getVertexRaw(k), direct.getVertexRaw(k));
assertEquals(children[i].getEdgeRaw(k), direct.getEdgeRaw(k));
}
// Test Contains() and MayIntersect().
assertTrue(cell.contains(children[i]));
assertTrue(cell.mayIntersect(children[i]));
assertTrue(!children[i].contains(cell));
assertTrue(cell.contains(children[i].getCenterRaw()));
for (int j = 0; j < 4; ++j) {
assertTrue(cell.contains(children[i].getVertexRaw(j)));
if (j != i) {
assertTrue(!children[i].contains(children[j].getCenterRaw()));
assertTrue(!children[i].mayIntersect(children[j]));
}
}
// Test GetCapBound and GetRectBound.
S2Cap parentCap = cell.getCapBound();
S2LatLngRect parentRect = cell.getRectBound();
if (cell.contains(new S2Point(0, 0, 1))
|| cell.contains(new S2Point(0, 0, -1))) {
assertTrue(parentRect.lng().isFull());
}
S2Cap childCap = children[i].getCapBound();
S2LatLngRect childRect = children[i].getRectBound();
assertTrue(childCap.contains(children[i].getCenter()));
assertTrue(childRect.contains(children[i].getCenterRaw()));
assertTrue(parentCap.contains(children[i].getCenter()));
assertTrue(parentRect.contains(children[i].getCenterRaw()));
for (int j = 0; j < 4; ++j) {
assertTrue(childCap.contains(children[i].getVertex(j)));
assertTrue(childRect.contains(children[i].getVertex(j)));
assertTrue(childRect.contains(children[i].getVertexRaw(j)));
assertTrue(parentCap.contains(children[i].getVertex(j)));
if (!parentRect.contains(children[i].getVertex(j))) {
System.out.println("cell: " + cell + " i: " + i + " j: " + j);
System.out.println("Children " + i + ": " + children[i]);
System.out.println("Parent rect: " + parentRect);
System.out.println("Vertex raw(j) " + children[i].getVertex(j));
System.out.println("Latlng of vertex: " + new S2LatLng(children[i].getVertex(j)));
cell.getRectBound();
}
assertTrue(parentRect.contains(children[i].getVertex(j)));
if (!parentRect.contains(children[i].getVertexRaw(j))) {
System.out.println("cell: " + cell + " i: " + i + " j: " + j);
System.out.println("Children " + i + ": " + children[i]);
System.out.println("Parent rect: " + parentRect);
System.out.println("Vertex raw(j) " + children[i].getVertexRaw(j));
System.out.println("Latlng of vertex: " + new S2LatLng(children[i].getVertexRaw(j)));
cell.getRectBound();
}
assertTrue(parentRect.contains(children[i].getVertexRaw(j)));
if (j != i) {
// The bounding caps and rectangles should be tight enough so that
// they exclude at least two vertices of each adjacent cell.
int capCount = 0;
int rectCount = 0;
for (int k = 0; k < 4; ++k) {
if (childCap.contains(children[j].getVertex(k))) {
++capCount;
}
if (childRect.contains(children[j].getVertexRaw(k))) {
++rectCount;
}
}
assertTrue(capCount <= 2);
if (childRect.latLo().radians() > -S2.M_PI_2
&& childRect.latHi().radians() < S2.M_PI_2) {
// Bounding rectangles may be too large at the poles because the
// pole itself has an arbitrary fixed longitude.
assertTrue(rectCount <= 2);
}
}
}
// Check all children for the first few levels, and then sample randomly.
// Also subdivide one corner cell, one edge cell, and one center cell
// so that we have a better chance of sample the minimum metric values.
boolean forceSubdivide = false;
S2Point center = S2Projections.getNorm(children[i].face());
S2Point edge = S2Point.add(center, S2Projections.getUAxis(children[i].face()));
S2Point corner = S2Point.add(edge, S2Projections.getVAxis(children[i].face()));
for (int j = 0; j < 4; ++j) {
S2Point p = children[i].getVertexRaw(j);
if (p.equals(center) || p.equals(edge) || p.equals(corner)) {
forceSubdivide = true;
}
}
if (forceSubdivide || cell.level() < (DEBUG_MODE ? 5 : 6)
|| random(DEBUG_MODE ? 10 : 4) == 0) {
testSubdivide(children[i]);
}
}
// Check sum of child areas equals parent area.
//
// For ExactArea(), the best relative error we can expect is about 1e-6
// because the precision of the unit vector coordinates is only about 1e-15
// and the edge length of a leaf cell is about 1e-9.
//
// For ApproxArea(), the areas are accurate to within a few percent.
//
// For AverageArea(), the areas themselves are not very accurate, but
// the average area of a parent is exactly 4 times the area of a child.
assertTrue(Math.abs(Math.log(exactArea / cell.exactArea())) <= Math
.abs(Math.log(1 + 1e-6)));
assertTrue(Math.abs(Math.log(approxArea / cell.approxArea())) <= Math
.abs(Math.log(1.03)));
assertTrue(Math.abs(Math.log(averageArea / cell.averageArea())) <= Math
.abs(Math.log(1 + 1e-15)));
}
public void testMinMaxAvg(String label, int level, double count,
double absError, double minValue, double maxValue, double avgValue,
S2.Metric minMetric, S2.Metric maxMetric, S2.Metric avgMetric) {
// All metrics are minimums, maximums, or averages of differential
// quantities, and therefore will not be exact for cells at any finite
// level. The differential minimum is always a lower bound, and the maximum
// is always an upper bound, but these minimums and maximums may not be
// achieved for two different reasons. First, the cells at each level are
// sampled and we may miss the most extreme examples. Second, the actual
// metric for a cell is obtained by integrating the differential quantity,
// which is not constant across the cell. Therefore cells at low levels
// (bigger cells) have smaller variations.
//
// The "tolerance" below is an attempt to model both of these effects.
// At low levels, error is dominated by the variation of differential
// quantities across the cells, while at high levels error is dominated by
// the effects of random sampling.
double tolerance = (maxMetric.getValue(level) - minMetric.getValue(level))
/ Math.sqrt(Math.min(count, 0.5 * (1L << level))) * 10;
if (tolerance == 0) {
tolerance = absError;
}
double minError = minValue - minMetric.getValue(level);
double maxError = maxMetric.getValue(level) - maxValue;
double avgError = Math.abs(avgMetric.getValue(level) - avgValue);
System.out.printf(
"%-10s (%6.0f samples, tolerance %8.3g) - min (%9.3g : %9.3g) "
+ "max (%9.3g : %9.3g), avg (%9.3g : %9.3g)\n", label, count,
tolerance, minError / minValue, minError / tolerance, maxError
/ maxValue, maxError / tolerance, avgError / avgValue, avgError
/ tolerance);
assertTrue(minMetric.getValue(level) <= minValue + absError);
assertTrue(minMetric.getValue(level) >= minValue - tolerance);
System.out.println("Level: " + maxMetric.getValue(level) + " max " + (maxValue + tolerance));
assertTrue(maxMetric.getValue(level) <= maxValue + tolerance);
assertTrue(maxMetric.getValue(level) >= maxValue - absError);
assertDoubleNear(avgMetric.getValue(level), avgValue, 10 * tolerance);
}
public void testSubdivide() {
for (int face = 0; face < 6; ++face) {
testSubdivide(S2Cell.fromFacePosLevel(face, (byte) 0, 0));
}
// The maximum edge *ratio* is the ratio of the longest edge of any cell to
// the shortest edge of any cell at the same level (and similarly for the
// maximum diagonal ratio).
//
// The maximum edge *aspect* is the maximum ratio of the longest edge of a
// cell to the shortest edge of that same cell (and similarly for the
// maximum diagonal aspect).
System.out
.printf("Level Area Edge Diag Approx Average\n");
System.out
.printf(" Ratio Ratio Aspect Ratio Aspect Min Max Min Max\n");
for (int i = 0; i <= S2CellId.MAX_LEVEL; ++i) {
LevelStats s = levelStats.get(i);
if (s.count > 0) {
s.avgArea /= s.count;
s.avgWidth /= s.count;
s.avgEdge /= s.count;
s.avgDiag /= s.count;
s.avgAngleSpan /= s.count;
}
System.out.printf(
"%5d %6.3f %6.3f %6.3f %6.3f %6.3f %6.3f %6.3f %6.3f %6.3f\n", i,
s.maxArea / s.minArea, s.maxEdge / s.minEdge, s.maxEdgeAspect,
s.maxDiag / s.minDiag, s.maxDiagAspect, s.minApproxRatio,
s.maxApproxRatio, S2Cell.averageArea(i) / s.maxArea, S2Cell
.averageArea(i)
/ s.minArea);
}
// Now check the validity of the S2 length and area metrics.
for (int i = 0; i <= S2CellId.MAX_LEVEL; ++i) {
LevelStats s = levelStats.get(i);
if (s.count == 0) {
continue;
}
System.out.printf(
"Level %2d - metric (error/actual : error/tolerance)\n", i);
// The various length calculations are only accurate to 1e-15 or so,
// so we need to allow for this amount of discrepancy with the theoretical
// minimums and maximums. The area calculation is accurate to about 1e-15
// times the cell width.
testMinMaxAvg("area", i, s.count, 1e-15 * s.minWidth, s.minArea,
s.maxArea, s.avgArea, S2Projections.MIN_AREA, S2Projections.MAX_AREA,
S2Projections.AVG_AREA);
testMinMaxAvg("width", i, s.count, 1e-15, s.minWidth, s.maxWidth,
s.avgWidth, S2Projections.MIN_WIDTH, S2Projections.MAX_WIDTH,
S2Projections.AVG_WIDTH);
testMinMaxAvg("edge", i, s.count, 1e-15, s.minEdge, s.maxEdge,
s.avgEdge, S2Projections.MIN_EDGE, S2Projections.MAX_EDGE,
S2Projections.AVG_EDGE);
testMinMaxAvg("diagonal", i, s.count, 1e-15, s.minDiag, s.maxDiag,
s.avgDiag, S2Projections.MIN_DIAG, S2Projections.MAX_DIAG,
S2Projections.AVG_DIAG);
testMinMaxAvg("angle span", i, s.count, 1e-15, s.minAngleSpan,
s.maxAngleSpan, s.avgAngleSpan, S2Projections.MIN_ANGLE_SPAN,
S2Projections.MAX_ANGLE_SPAN, S2Projections.AVG_ANGLE_SPAN);
// The aspect ratio calculations are ratios of lengths and are therefore
// less accurate at higher subdivision levels.
assertTrue(s.maxEdgeAspect <= S2Projections.MAX_EDGE_ASPECT + 1e-15
* (1 << i));
assertTrue(s.maxDiagAspect <= S2Projections.MAX_DIAG_ASPECT + 1e-15
* (1 << i));
}
}
static final int MAX_LEVEL = DEBUG_MODE ? 6 : 10;
public void expandChildren1(S2Cell cell) {
S2Cell[] children = new S2Cell[4];
assertTrue(cell.subdivide(children));
if (children[0].level() < MAX_LEVEL) {
for (int pos = 0; pos < 4; ++pos) {
expandChildren1(children[pos]);
}
}
}
public void expandChildren2(S2Cell cell) {
S2CellId id = cell.id().childBegin();
for (int pos = 0; pos < 4; ++pos, id = id.next()) {
S2Cell child = new S2Cell(id);
if (child.level() < MAX_LEVEL) {
expandChildren2(child);
}
}
}
}