1 /* 2 * Copyright 2005 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 package com.google.common.geometry; 17 18 import com.google.common.collect.Lists; 19 20 import java.util.ArrayList; 21 import java.util.HashSet; 22 import java.util.List; 23 import java.util.Set; 24 import java.util.logging.Logger; 25 26 public strictfp class S2CellUnionTest extends GeometryTestCase { 27 public static Logger logger = Logger.getLogger(S2CellUnionTest.class.getName()); 28 testBasic()29 public void testBasic() { 30 logger.info("TestBasic"); 31 32 S2CellUnion empty = new S2CellUnion(); 33 ArrayList<S2CellId> ids = Lists.newArrayList(); 34 empty.initFromCellIds(ids); 35 assertEquals(0, empty.size()); 36 37 S2CellId face1Id = S2CellId.fromFacePosLevel(1, 0, 0); 38 S2CellUnion face1Union = new S2CellUnion(); 39 ids.add(face1Id); 40 face1Union.initFromCellIds(ids); 41 assertEquals(1, face1Union.size()); 42 assertEquals(face1Id, face1Union.cellId(0)); 43 44 S2CellId face2Id = S2CellId.fromFacePosLevel(2, 0, 0); 45 S2CellUnion face2Union = new S2CellUnion(); 46 ArrayList<Long> cellids = Lists.newArrayList(); 47 cellids.add(face2Id.id()); 48 face2Union.initFromIds(cellids); 49 assertEquals(1, face2Union.size()); 50 assertEquals(face2Id, face2Union.cellId(0)); 51 52 S2Cell face1Cell = new S2Cell(face1Id); 53 S2Cell face2Cell = new S2Cell(face2Id); 54 assertTrue(face1Union.contains(face1Cell)); 55 assertTrue(!face1Union.contains(face2Cell)); 56 } 57 testContainsCellUnion()58 public void testContainsCellUnion() { 59 logger.info("TestContainsCellUnion"); 60 61 Set<S2CellId> randomCells = new HashSet<S2CellId>(); 62 for (int i = 0; i < 100; i++) { 63 randomCells.add(getRandomCellId(S2CellId.MAX_LEVEL)); 64 } 65 66 S2CellUnion union = new S2CellUnion(); 67 union.initFromCellIds(Lists.newArrayList(randomCells)); 68 69 // Add one more 70 while (!randomCells.add(getRandomCellId(S2CellId.MAX_LEVEL))) { 71 } 72 73 S2CellUnion unionPlusOne = new S2CellUnion(); 74 unionPlusOne.initFromCellIds(Lists.newArrayList(randomCells)); 75 76 assertTrue(unionPlusOne.contains(union)); 77 assertFalse(union.contains(unionPlusOne)); 78 79 // Build the set of parent cells and check containment 80 Set<S2CellId> parents = new HashSet<S2CellId>(); 81 for (S2CellId cellId : union) { 82 parents.add(cellId.parent()); 83 } 84 85 S2CellUnion parentUnion = new S2CellUnion(); 86 parentUnion.initFromCellIds(Lists.newArrayList(parents)); 87 88 assertTrue(parentUnion.contains(union)); 89 assertFalse(union.contains(parentUnion)); 90 } 91 addCells(S2CellId id, boolean selected, List<S2CellId> input, ArrayList<S2CellId> expected)92 private void addCells(S2CellId id, boolean selected, List<S2CellId> input, 93 ArrayList<S2CellId> expected) { 94 // Decides whether to add "id" and/or some of its descendants to the 95 // test case. If "selected" is true, then the region covered by "id" 96 // *must* be added to the test case (either by adding "id" itself, or 97 // some combination of its descendants, or both). If cell ids are to 98 // the test case "input", then the corresponding expected result after 99 // simplification is added to "expected". 100 101 if (id.equals(S2CellId.none())) { 102 // Initial call: decide whether to add cell(s) from each face. 103 for (int face = 0; face < 6; ++face) { 104 addCells(S2CellId.fromFacePosLevel(face, 0, 0), false, input, expected); 105 } 106 return; 107 } 108 if (id.isLeaf()) { 109 // The rnd.OneIn() call below ensures that the parent of a leaf cell 110 // will always be selected (if we make it that far down the hierarchy). 111 assertTrue(selected); 112 input.add(id); 113 return; 114 } 115 // The following code ensures that the probability of selecting a cell 116 // at each level is approximately the same, i.e. we test normalization 117 // of cells at all levels. 118 if (!selected && random(S2CellId.MAX_LEVEL - id.level()) != 0) { 119 // Once a cell has been selected, the expected output is predetermined. 120 // We then make sure that cells are selected that will normalize to 121 // the desired output. 122 expected.add(id); 123 selected = true; 124 } 125 126 // With the rnd.OneIn() constants below, this function adds an average 127 // of 5/6 * (kMaxLevel - level) cells to "input" where "level" is the 128 // level at which the cell was first selected (level 15 on average). 129 // Therefore the average number of input cells in a test case is about 130 // (5/6 * 15 * 6) = 75. The average number of output cells is about 6. 131 132 // If a cell is selected, we add it to "input" with probability 5/6. 133 boolean added = false; 134 if (selected && random(6) != 0) { 135 input.add(id); 136 added = true; 137 } 138 int numChildren = 0; 139 S2CellId child = id.childBegin(); 140 for (int pos = 0; pos < 4; ++pos, child = child.next()) { 141 // If the cell is selected, on average we recurse on 4/12 = 1/3 child. 142 // This intentionally may result in a cell and some of its children 143 // being included in the test case. 144 // 145 // If the cell is not selected, on average we recurse on one child. 146 // We also make sure that we do not recurse on all 4 children, since 147 // then we might include all 4 children in the input case by accident 148 // (in which case the expected output would not be correct). 149 if (random(selected ? 12 : 4) == 0 && numChildren < 3) { 150 addCells(child, selected, input, expected); 151 ++numChildren; 152 } 153 // If this cell was selected but the cell itself was not added, we 154 // must ensure that all 4 children (or some combination of their 155 // descendents) are added. 156 if (selected && !added) { 157 addCells(child, selected, input, expected); 158 } 159 } 160 } 161 testNormalize()162 public void testNormalize() { 163 logger.info("TestNormalize"); 164 165 // Try a bunch of random test cases, and keep track of average 166 // statistics for normalization (to see if they agree with the 167 // analysis above). 168 S2CellUnion cellunion = new S2CellUnion(); 169 double inSum = 0, outSum = 0; 170 final int kIters = 2000; 171 for (int i = 0; i < kIters; ++i) { 172 ArrayList<S2CellId> input = Lists.newArrayList(); 173 ArrayList<S2CellId> expected = Lists.newArrayList(); 174 addCells(S2CellId.none(), false, input, expected); 175 inSum += input.size(); 176 outSum += expected.size(); 177 cellunion.initFromCellIds(input); 178 assertEquals(cellunion.size(), expected.size()); 179 180 assertEquals(expected, cellunion.cellIds()); 181 182 // Test GetCapBound(). 183 S2Cap cap = cellunion.getCapBound(); 184 for (int k = 0; k < cellunion.size(); ++k) { 185 assertTrue(cap.contains(new S2Cell(cellunion.cellId(k)))); 186 } 187 188 // Test Contains(S2CellId) and Intersects(S2CellId). 189 for (int j = 0; j < input.size(); ++j) { 190 assertTrue(cellunion.contains(input.get(j))); 191 assertTrue(cellunion.intersects(input.get(j))); 192 if (!input.get(j).isFace()) { 193 assertTrue(cellunion.intersects(input.get(j).parent())); 194 if (input.get(j).level() > 1) { 195 assertTrue(cellunion.intersects(input.get(j).parent().parent())); 196 assertTrue(cellunion.intersects(input.get(j).parent(0))); 197 } 198 } 199 if (!input.get(j).isLeaf()) { 200 assertTrue(cellunion.contains(input.get(j).childBegin())); 201 assertTrue(cellunion.intersects(input.get(j).childBegin())); 202 assertTrue(cellunion.contains(input.get(j).childEnd().prev())); 203 assertTrue(cellunion.intersects(input.get(j).childEnd().prev())); 204 assertTrue(cellunion.contains(input.get(j).childBegin(S2CellId.MAX_LEVEL))); 205 assertTrue(cellunion.intersects(input.get(j).childBegin(S2CellId.MAX_LEVEL))); 206 } 207 } 208 for (int j = 0; j < expected.size(); ++j) { 209 if (!expected.get(j).isFace()) { 210 assertTrue(!cellunion.contains(expected.get(j).parent())); 211 assertTrue(!cellunion.contains(expected.get(j).parent(0))); 212 } 213 } 214 215 // Test contains(S2CellUnion) and intersects(S2CellUnion) 216 ArrayList<S2CellId> x = Lists.newArrayList(); 217 ArrayList<S2CellId> y = Lists.newArrayList(); 218 ArrayList<S2CellId> xOrY = Lists.newArrayList(); 219 ArrayList<S2CellId> xAndY = Lists.newArrayList(); 220 for (int j = 0; j < input.size(); ++j) { 221 boolean inX = random(2) == 0; 222 boolean inY = random(2) == 0; 223 if (inX) { 224 x.add(input.get(j)); 225 } 226 if (inY) { 227 y.add(input.get(j)); 228 } 229 if (inX || inY) { 230 xOrY.add(input.get(j)); 231 } 232 } 233 S2CellUnion xCells = new S2CellUnion(); 234 S2CellUnion yCells = new S2CellUnion(); 235 S2CellUnion xOrYExpected = new S2CellUnion(); 236 S2CellUnion xAndYExpected = new S2CellUnion(); 237 xCells.initFromCellIds(x); 238 yCells.initFromCellIds(y); 239 xOrYExpected.initFromCellIds(xOrY); 240 241 S2CellUnion xOrYCells = new S2CellUnion(); 242 xOrYCells.getUnion(xCells, yCells); 243 assertEquals(xOrYExpected, xOrYCells); 244 245 // Compute the intersection of "x" with each cell of "y", 246 // check that this intersection is correct, and append the 247 // results to xAndYExpected. 248 for (int j = 0; j < yCells.size(); ++j) { 249 S2CellId yId = yCells.cellId(j); 250 S2CellUnion u = new S2CellUnion(); 251 u.getIntersection(xCells, yId); 252 for (int k = 0; k < xCells.size(); ++k) { 253 S2CellId xId = xCells.cellId(k); 254 if (xId.contains(yId)) { 255 assertEquals(1, u.size()); 256 assertEquals(yId, u.cellId(0)); 257 } else if (yId.contains(xId)) { 258 if (!u.contains(xId)) { 259 u.getIntersection(xCells, yId); 260 } 261 assertTrue(u.contains(xId)); 262 } 263 } 264 for (int k = 0; k < u.size(); ++k) { 265 assertTrue(xCells.contains(u.cellId(k))); 266 assertTrue(yId.contains(u.cellId(k))); 267 } 268 xAndY.addAll(u.cellIds()); 269 } 270 xAndYExpected.initFromCellIds(xAndY); 271 272 S2CellUnion xAndYCells = new S2CellUnion(); 273 xAndYCells.getIntersection(xCells, yCells); 274 assertEquals(xAndYExpected, xAndYCells); 275 276 ArrayList<S2CellId> test = Lists.newArrayList(); 277 ArrayList<S2CellId> dummy = Lists.newArrayList(); 278 279 addCells(S2CellId.none(), false, test, dummy); 280 for (int j = 0; j < test.size(); ++j) { 281 boolean contains = false, intersects = false; 282 for (int k = 0; k < expected.size(); ++k) { 283 if (expected.get(k).contains(test.get(j))) { 284 contains = true; 285 } 286 if (expected.get(k).intersects(test.get(j))) { 287 intersects = true; 288 } 289 } 290 assertEquals(cellunion.contains(test.get(j)), contains); 291 assertEquals(cellunion.intersects(test.get(j)), intersects); 292 } 293 294 } 295 } 296 getMaxAngle(S2CellUnion covering, S2Point axis)297 double getMaxAngle(S2CellUnion covering, S2Point axis) { 298 double maxAngle = 0; 299 for (int i = 0; i < covering.size(); ++i) { 300 S2Cell cell = new S2Cell(covering.cellId(i)); 301 S2Cap cellCap = cell.getCapBound(); 302 double angle = axis.angle(cellCap.axis()) + cellCap.angle().radians(); 303 maxAngle = Math.max(maxAngle, angle); 304 } 305 return maxAngle; 306 } 307 testExpand()308 public void testExpand() { 309 logger.info("TestExpand"); 310 311 // This test generates coverings for caps of random sizes, and expands 312 // the coverings by a random radius, and then make sure that the new 313 // covering covers the expanded cap. It also makes sure that the 314 // new covering is not too much larger than expected. 315 316 S2RegionCoverer coverer = new S2RegionCoverer(); 317 for (int i = 0; i < 1000; ++i) { 318 S2Cap cap = getRandomCap(S2Cell.averageArea(S2CellId.MAX_LEVEL), 4 * S2.M_PI); 319 320 // Expand the cap by a random factor whose log is uniformly distributed 321 // between 0 and log(1e2). 322 S2Cap expandedCap = 323 S2Cap.fromAxisHeight(cap.axis(), Math.min(2.0, Math.pow(1e2, rand.nextDouble()) 324 * cap.height())); 325 326 double radius = expandedCap.angle().radians() - cap.angle().radians(); 327 int maxLevelDiff = random(8); 328 329 S2CellUnion covering = new S2CellUnion(); 330 coverer.setMaxCells(1 + skewed(10)); 331 coverer.getCovering(cap, covering); 332 checkCovering(cap, covering, true, new S2CellId()); 333 334 double maxAngle = getMaxAngle(covering, cap.axis()); 335 int minLevel = S2CellId.MAX_LEVEL; 336 for (int j = 0; j < covering.size(); ++j) { 337 minLevel = Math.min(minLevel, covering.cellId(j).level()); 338 } 339 covering.expand(S1Angle.radians(radius), maxLevelDiff); 340 checkCovering(expandedCap, covering, false, new S2CellId()); 341 342 int expandLevel = 343 Math.min(minLevel + maxLevelDiff, S2Projections.MIN_WIDTH.getMaxLevel(radius)); 344 double expandedMaxAngle = getMaxAngle(covering, cap.axis()); 345 346 // If the covering includes a tiny cell along the boundary, in theory the 347 // maximum angle of the covering from the cap axis can increase by up to 348 // twice the maximum length of a cell diagonal. We allow for an increase 349 // of slightly more than this because cell bounding caps are not exact. 350 assertTrue(expandedMaxAngle - maxAngle <= 2.01 * S2Projections.MAX_DIAG 351 .getValue(expandLevel)); 352 } 353 } 354 testLeafCellsCovered()355 public void testLeafCellsCovered() { 356 S2CellUnion cellUnion = new S2CellUnion(); 357 358 // empty union 359 assertEquals(0, cellUnion.leafCellsCovered()); 360 361 ArrayList<S2CellId> ids = Lists.newArrayList(); 362 ids.add(S2CellId.fromFacePosLevel( 363 0, (1L << ((S2CellId.MAX_LEVEL << 1) - 1)), S2CellId.MAX_LEVEL)); 364 365 // One leaf on face 0. 366 cellUnion.initFromCellIds(ids); 367 assertEquals(1L, cellUnion.leafCellsCovered()); 368 369 // Face 0. 370 ids.add(S2CellId.fromFacePosLevel(0, 0, 0)); 371 cellUnion.initFromCellIds(ids); 372 assertEquals(1L << 60, cellUnion.leafCellsCovered()); 373 374 // Five faces. 375 cellUnion.expand(0); 376 assertEquals(5L << 60, cellUnion.leafCellsCovered()); 377 378 // Whole world. 379 cellUnion.expand(0); 380 assertEquals(6L << 60, cellUnion.leafCellsCovered()); 381 382 // Add some disjoint cells. 383 ids.add(S2CellId.fromFacePosLevel(1, 0, 1)); 384 ids.add(S2CellId.fromFacePosLevel(2, 0, 2)); 385 ids.add(S2CellId.fromFacePosLevel(2, (1L << 60), 2)); 386 ids.add(S2CellId.fromFacePosLevel(3, 0, 14)); 387 ids.add(S2CellId.fromFacePosLevel(4, (1L << 60), 15)); 388 ids.add(S2CellId.fromFacePosLevel(4, 0, 27)); 389 ids.add(S2CellId.fromFacePosLevel(5, 0, 30)); 390 cellUnion.initFromCellIds(ids); 391 long expected = 1L + (1L << 6) + (1L << 30) + (1L << 32) + (2L << 56) + (1L << 58) + (1L << 60); 392 assertEquals(expected, cellUnion.leafCellsCovered()); 393 } 394 395 testAverageBasedArea()396 public void testAverageBasedArea() { 397 S2CellUnion cellUnion = new S2CellUnion(); 398 399 // empty union 400 assertEquals(0.0, cellUnion.averageBasedArea()); 401 402 ArrayList<S2CellId> ids = Lists.newArrayList(); 403 ids.add(S2CellId.fromFacePosLevel(1, 0, 1)); 404 ids.add(S2CellId.fromFacePosLevel(5, 0, 30)); 405 cellUnion.initFromCellIds(ids); 406 407 double expected = S2Cell.averageArea(S2CellId.MAX_LEVEL) * (1L + (1L << 58)); 408 assertEquals(expected, cellUnion.averageBasedArea()); 409 } 410 testApproxArea()411 public void testApproxArea() { 412 S2CellUnion cellUnion = new S2CellUnion(); 413 414 // empty union 415 assertEquals(0.0, cellUnion.approxArea()); 416 417 ArrayList<S2CellId> ids = Lists.newArrayList(); 418 ids.add(S2CellId.fromFacePosLevel(1, 0, 1)); 419 ids.add(S2CellId.fromFacePosLevel(5, 0, 30)); 420 cellUnion.initFromCellIds(ids); 421 422 double expected = new S2Cell(ids.get(0)).approxArea() + new S2Cell(ids.get(1)).approxArea(); 423 assertEquals(expected, cellUnion.approxArea()); 424 } 425 testExactArea()426 public void testExactArea() { 427 S2CellUnion cellUnion = new S2CellUnion(); 428 429 // empty union 430 assertEquals(0.0, cellUnion.exactArea()); 431 432 ArrayList<S2CellId> ids = Lists.newArrayList(); 433 ids.add(S2CellId.fromFacePosLevel(1, 0, 1)); 434 ids.add(S2CellId.fromFacePosLevel(5, 0, 30)); 435 cellUnion.initFromCellIds(ids); 436 437 double expected = new S2Cell(ids.get(0)).exactArea() + new S2Cell(ids.get(1)).exactArea(); 438 assertEquals(expected, cellUnion.averageBasedArea()); 439 } 440 } 441