/* * 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 edgeCounts = new HashMap(); Map vertexCounts = new HashMap(); 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 = new ArrayList( 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); } } } }