• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2006 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 
17 package com.google.common.geometry;
18 
19 import com.google.common.collect.Lists;
20 
21 import java.util.List;
22 import java.util.logging.Logger;
23 
24 /**
25  * Tests for {@link S2Loop}.
26  *
27  */
28 public strictfp class S2PolygonBuilderTest extends GeometryTestCase {
29   private static final Logger log = Logger.getLogger(S2PolygonBuilderTest.class.getCanonicalName());
30 
31   // A chain represents either a polyline or a loop, depending
32   // on whether "closed" is true.
33   private class Chain {
34     String str;
35     boolean closed;
36 
Chain(String str, boolean closed)37     public Chain(String str, boolean closed) {
38       this.str = str;
39       this.closed = closed;
40     }
41   }
42 
43   private class TestCase {
44     // +1 = undirected, -1 = directed, 0 = either one
45     int undirectedEdges;
46 
47     // +1 = XOR, -1 = don't XOR, 0 = either one
48     int xorEdges;
49 
50     // Minimum and maximum merge distances for this test case in degrees.
51     double minMerge;
52     double maxMerge;
53 
54     // Each test case consists of a set of input loops and polylines.
55     Chain[] chainsIn;
56 
57     // The expected set of output loops, directed appropriately.
58     String[] loopsOut;
59 
60     // The expected number of unused edges.
61     int numUnusedEdges;
62 
TestCase(int undirectedEdges, int xorEdges, double minMerge, double maxMerge, Chain[] chainsIn, String[] loopsOut, int numUnusedEdges)63     public TestCase(int undirectedEdges,
64         int xorEdges,
65         double minMerge,
66         double maxMerge,
67         Chain[] chainsIn,
68         String[] loopsOut,
69         int numUnusedEdges) {
70       this.undirectedEdges = undirectedEdges;
71       this.xorEdges = xorEdges;
72       this.minMerge = minMerge;
73       this.maxMerge = maxMerge;
74       this.chainsIn = chainsIn;
75       this.loopsOut = loopsOut;
76       this.numUnusedEdges = numUnusedEdges;
77     }
78   }
79 
80   TestCase[] testCases = new TestCase[] {
81   // 0: No loops.
82   new TestCase(0, 0, 0.0, 10.0, new Chain[] {new Chain(null, false)}, new String[] {}, 0),
83 
84       // 1: One loop with some extra edges.
85       new TestCase(0,
86           0,
87           0.0,
88           4.0,
89           new Chain[] {new Chain("0:0, 0:10, 10:5", true), new Chain("0:0, 5:5", false),
90               new Chain("10:5, 20:7, 30:10, 40:15, 50:3, 60:-20", false)},
91           new String[] {"0:0, 0:10, 10:5"},
92           6),
93 
94       // 2: One loop that has an edge removed by XORing, plus lots of
95       // extra edges.
96       new TestCase(0, 1, 0.0, 1.0, // XOR
97           new Chain[] {new Chain("0:0, 0:10, 5:15, 10:10, 10:0", true),
98               new Chain("10:10, 12:12, 14:14, 16:16, 18:18", false),
99               new Chain("14:14, 14:16, 14:18, 14:20", false),
100               new Chain("14:18, 16:20, 18:22", false),
101               new Chain("18:12, 16:12, 14:12, 12:12", false),
102               new Chain("20:18, 18:16, 16:14, 14:12", false),
103               new Chain("20:14, 18:14, 16:14", false),
104               new Chain("5:15, 0:10", false)},
105           new String[] {},
106           21),
107 
108       // 3: Three loops (two shells and one hole) that combine into one.
109       new TestCase(0, 1, 0.0, 4.0, // XOR
110           new Chain[] {new Chain("0:0, 0:10, 5:10, 10:10, 10:5, 10:0", true),
111               new Chain("0:10, 0:15, 5:15, 5:10", true),
112               new Chain("10:10, 5:10, 5:5, 10:5", true), },
113           new String[] {"0:0, 0:10, 0:15, 5:15, 5:10, 5:5, 10:5, 10:0"},
114           0),
115 
116       // 4: A big CCW triangle contained 3 CW triangular holes. The whole thing
117       // looks like a pyramid of nine small triangles (with two extra edges).
118       new TestCase(-1, 0, 0.0, 0.9, // Directed edges required for unique result.
119           new Chain[] {new Chain("0:0, 0:2, 0:4, 0:6, 1:5, 2:4, 3:3, 2:2, 1:1", true),
120               new Chain("0:2, 1:1, 1:3", true),
121               new Chain("0:4, 1:3, 1:5", true),
122               new Chain("1:3, 2:2, 2:4", true),
123               new Chain("0:0, 0:1", false),
124               new Chain("1:3, 5:7", false)},
125           new String[] {"0:0, 0:2, 1:1",
126               "0:2, 0:4, 1:3",
127               "0:4, 0:6, 1:5",
128               "1:1, 1:3, 2:2",
129               "1:3, 1:5, 2:4",
130               "2:2, 2:4, 3:3"},
131           2),
132 
133       // 5: A square divided into four subsquares. In this case we want
134       // to extract the four loops rather than taking their union.
135       // There are four extra edges as well.
136       new TestCase(0, -1, 0.0, 4.0, // Don't XOR
137           new Chain[] {new Chain("0:0, 0:5, 5:5, 5:0", true),
138               new Chain("0:5, 0:10, 5:10, 5:5", true),
139               new Chain("5:0, 5:5, 10:5, 10:0", true),
140               new Chain("5:5, 5:10, 10:10, 10:5", true),
141               new Chain("0:10, 0:15, 0:20", false),
142               new Chain("20:0, 15:0, 10:0", false)},
143           new String[] {"0:0, 0:5, 5:5, 5:0", "0:5, 0:10, 5:10, 5:5", "5:0, 5:5, 10:5, 10:0",
144               "5:5, 5:10, 10:10, 10:5"},
145           4),
146 
147       // 6: Five nested loops that touch at a point.
148       new TestCase(0,
149           0,
150           0.0,
151           0.8,
152           new Chain[] {new Chain("0:0, 0:10, 10:10, 10:0", true),
153               new Chain("0:0, 1:9, 9:9, 9:1", true), new Chain("0:0, 2:8, 8:8, 8:2", true),
154               new Chain("0:0, 3:7, 7:7, 7:3", true), new Chain("0:0, 4:6, 6:6, 6:4", true)},
155           new String[] {"0:0, 0:10, 10:10, 10:0", "0:0, 1:9, 9:9, 9:1", "0:0, 2:8, 8:8, 8:2",
156               "0:0, 3:7, 7:7, 7:3", "0:0, 4:6, 6:6, 6:4"},
157           0),
158 
159 
160       // 7: Four diamonds nested within each other touching at two points.
161       new TestCase(-1, 0, 0.0, 4.0, // Directed edges required for unique result.
162           new Chain[] {new Chain("0:-20, -10:0, 0:20, 10:0", true),
163               new Chain("0:10, -10:0, 0:-10, 10:0", true),
164               new Chain("0:-10, -5:0, 0:10, 5:0", true), new Chain("0:5, -5:0, 0:-5, 5:0", true)},
165           new String[] {"0:-20, -10:0, 0:-10, 10:0", "0:-10, -5:0, 0:-5, 5:0",
166               "0:5, -5:0, 0:10, 5:0", "0:10, -10:0, 0:20, 10:0"},
167           0),
168 
169       // 8: Seven diamonds nested within each other touching at one
170       // point between each nested pair.
171       new TestCase(0,
172           0,
173           0.0,
174           9.0,
175           new Chain[] {new Chain("0:-70, -70:0, 0:70, 70:0", true),
176               new Chain("0:-70, -60:0, 0:60, 60:0", true),
177               new Chain("0:-50, -60:0, 0:50, 50:0", true),
178               new Chain("0:-40, -40:0, 0:50, 40:0", true),
179               new Chain("0:-30, -30:0, 0:30, 40:0", true),
180               new Chain("0:-20, -20:0, 0:30, 20:0", true),
181               new Chain("0:-10, -20:0, 0:10, 10:0", true)},
182           new String[] {"0:-70, -70:0, 0:70, 70:0",
183               "0:-70, -60:0, 0:60, 60:0",
184               "0:-50, -60:0, 0:50, 50:0",
185               "0:-40, -40:0, 0:50, 40:0",
186               "0:-30, -30:0, 0:30, 40:0",
187               "0:-20, -20:0, 0:30, 20:0",
188               "0:-10, -20:0, 0:10, 10:0"},
189           0),
190 
191       // 9: A triangle and a self-intersecting bowtie.
192       new TestCase(0,
193           0,
194           0.0,
195           4.0,
196           new Chain[] {new Chain("0:0, 0:10, 5:5", true), new Chain("0:20, 0:30, 10:20", false),
197               new Chain("10:20, 10:30, 0:20", false)},
198           new String[] {"0:0, 0:10, 5:5"},
199           4),
200 
201       // 10: Two triangles that intersect each other.
202       new TestCase(0,
203           0,
204           0.0,
205           2.0,
206           new Chain[] {new Chain("0:0, 0:10, 5:5", true), new Chain("2:2, 2:12, 7:7", true)},
207           new String[] {},
208           6),
209 
210       // 11: Four squares that combine to make a big square. The nominal
211       // edges of the square are at +/-8.5 degrees in latitude and longitude.
212       // All vertices except the center vertex are perturbed by up to 0.5
213       // degrees in latitude and/or longitude. The various copies of the
214       // center vertex are misaligned by more than this (i.e. they are
215       // structured as a tree where adjacent vertices are separated by at
216       // most 1 degree in latitude and/or longitude) so that the clustering
217       // algorithm needs more than one iteration to find them all. Note that
218       // the merged position of this vertex doesn't matter because it is XORed
219       // away in the output.
220       new TestCase(0, 1, 1.5, 5.8, // XOR, min_merge > sqrt(2), max_merge < 6.
221           new Chain[] {new Chain("-8:-8, -8:0", false),
222               new Chain("-8:1, -8:8", false),
223               new Chain("0:-9, -2:0", false),
224               new Chain("-1:1, 1:9", false),
225               new Chain("0:8, 2:2", false),
226               new Chain("0:-2, 1:-8", false),
227               new Chain("8:9, 9:1", false),
228               new Chain("9:0, 8:-9", false),
229               new Chain("9:-9, 0:-8", false),
230               new Chain("1:-9, -9:-9", false),
231               new Chain("8:0, 1:0", false),
232               new Chain("1:2, -8:0", false),
233               new Chain("-8:1, 1:-1", false),
234               new Chain("0:1, 8:1", false),
235               new Chain("-9:8, 1:8", false),
236               new Chain("0:9, 8:8", false)},
237           new String[] {"8.5:8.5, 8.5:0.5, 8.5:-8.5, 0.5:-8.5, "
238               + "-8.5:-8.5, -8.5:0.5, -8.5:8.5, 0.5:8.5"},
239           0)};
240 
getVertices(String str, S2Point x, S2Point y, S2Point z, double maxPerturbation, List<S2Point> vertices)241   private void getVertices(String str,
242       S2Point x,
243       S2Point y,
244       S2Point z,
245       double maxPerturbation,
246       List<S2Point> vertices) {
247 
248     // Parse the vertices, perturb them if desired, and transform them into the
249     // given frame.
250     S2Polyline line = makePolyline(str);
251 
252     for (int i = 0; i < line.numVertices(); ++i) {
253       S2Point p = line.vertex(i);
254       // (p[0]*x + p[1]*y + p[2]*z).Normalize()
255       S2Point axis = S2Point.normalize(
256           S2Point.add(S2Point.add(S2Point.mul(x, p.x), S2Point.mul(y, p.y)), S2Point.mul(z, p.z)));
257       S2Cap cap = S2Cap.fromAxisAngle(axis, S1Angle.radians(maxPerturbation));
258       vertices.add(samplePoint(cap));
259     }
260   }
261 
loopsEqual(S2Loop a, S2Loop b, double maxError)262   private boolean loopsEqual(S2Loop a, S2Loop b, double maxError) {
263     // Return true if two loops have the same cyclic vertex sequence.
264 
265     if (a.numVertices() != b.numVertices()) {
266       return false;
267     }
268     for (int offset = 0; offset < a.numVertices(); ++offset) {
269       if (S2.approxEquals(a.vertex(offset), b.vertex(0), maxError)) {
270         boolean success = true;
271         for (int i = 0; i < a.numVertices(); ++i) {
272           if (!S2.approxEquals(a.vertex(i + offset), b.vertex(i), maxError)) {
273             success = false;
274             break;
275           }
276         }
277         if (success) {
278           return true;
279         }
280         // Otherwise continue looping. There may be more than one candidate
281         // starting offset since vertices are only matched approximately.
282       }
283     }
284     return false;
285   }
286 
findLoop(S2Loop loop, List<S2Loop> candidates, double maxError)287   private boolean findLoop(S2Loop loop, List<S2Loop> candidates, double maxError) {
288     for (int i = 0; i < candidates.size(); ++i) {
289       if (loopsEqual(loop, candidates.get(i), maxError)) {
290         return true;
291       }
292     }
293     return false;
294   }
295 
findMissingLoops( List<S2Loop> actual, List<S2Loop> expected, double maxError, String label)296   boolean findMissingLoops(
297       List<S2Loop> actual, List<S2Loop> expected, double maxError, String label) {
298     // Dump any loops from "actual" that are not present in "expected".
299     boolean found = false;
300     for (int i = 0; i < actual.size(); ++i) {
301       if (findLoop(actual.get(i), expected, maxError)) {
302         continue;
303       }
304       System.err.print(label + " loop " + i + ":\n");
305       S2Loop loop = actual.get(i);
306       for (int j = 0; j < loop.numVertices(); ++j) {
307         S2Point p = loop.vertex(j);
308         System.err.print("   [" + p.x + ", " + p.y + ", " + p.z + "]\n");
309       }
310       found = true;
311     }
312     return found;
313   }
314 
addChain(Chain chain, S2Point x, S2Point y, S2Point z, double maxPerturbation, S2PolygonBuilder builder)315   void addChain(Chain chain,
316       S2Point x,
317       S2Point y,
318       S2Point z,
319       double maxPerturbation,
320       S2PolygonBuilder builder) {
321 
322     // Transform the given edge chain to the frame (x,y,z), perturb each vertex
323     // up to the given distance, and add it to the builder.
324 
325     List<S2Point> vertices = Lists.newArrayList();
326     getVertices(chain.str, x, y, z, maxPerturbation, vertices);
327     if (chain.closed) {
328       vertices.add(vertices.get(0));
329     }
330     for (int i = 1; i < vertices.size(); ++i) {
331       builder.addEdge(vertices.get(i - 1), vertices.get(i));
332     }
333   }
334 
evalTristate(int state)335   boolean evalTristate(int state) {
336     return (state > 0) ? true : (state < 0) ? false : (rand.nextDouble() > 0.5);
337   }
338 
testBuilder(TestCase test)339   boolean testBuilder(TestCase test) {
340     for (int iter = 0; iter < 200; ++iter) {
341       // Initialize to the default options, which are changed below
342       S2PolygonBuilder.Options options = S2PolygonBuilder.Options.DIRECTED_XOR;
343 
344       options.setUndirectedEdges(evalTristate(test.undirectedEdges));
345       options.setXorEdges(evalTristate(test.xorEdges));
346 
347       // Each test has a minimum and a maximum merge distance. The merge
348       // distance must be at least the given minimum to ensure that all expected
349       // merging will take place, and it must be at most the given maximum to
350       // ensure that no unexpected merging takes place.
351       //
352       // If the minimum and maximum values are different, we have some latitude
353       // to perturb the vertices as long as the merge distance is adjusted
354       // appropriately. If "p" is the maximum perturbation distance, "min" and
355       // "max" are the min/max merge distances, and "m" is the actual merge
356       // distance for this test, we require that
357       //
358       // x >= min + 2*p and x <= max - 2*p .
359       //
360       // This implies that p <= 0.25 * (max - min). We choose "p" so that it is
361       // zero half of the time, and otherwise chosen randomly up to this limit.
362 
363       double minMerge = S1Angle.degrees(test.minMerge).radians();
364       double maxMerge = S1Angle.degrees(test.maxMerge).radians();
365       double r = Math.max(0.0, 2 * rand.nextDouble() - 1);
366       double maxPerturbation = r * 0.25 * (maxMerge - minMerge);
367 
368       // Now we set the merge distance chosen randomly within the limits above
369       // (min + 2*p and max - 2*p). Half of the time we set the merge distance
370       // to the minimum value.
371 
372       r = Math.max(0.0, 2 * rand.nextDouble() - 1);
373       options.setMergeDistance(S1Angle.radians(
374           minMerge + 2 * maxPerturbation + r * (maxMerge - minMerge - 4 * maxPerturbation)));
375 
376       options.setValidate(true);
377       S2PolygonBuilder builder = new S2PolygonBuilder(options);
378 
379       // On each iteration we randomly rotate the test case around the sphere.
380       // This causes the S2PolygonBuilder to choose different first edges when
381       // trying to build loops.
382       S2Point x = randomPoint();
383       S2Point y = S2Point.normalize(S2Point.crossProd(x, randomPoint()));
384       S2Point z = S2Point.normalize(S2Point.crossProd(x, y));
385 
386       for (Chain chain : test.chainsIn) {
387         addChain(chain, x, y, z, maxPerturbation, builder);
388       }
389       List<S2Loop> loops = Lists.newArrayList();
390       List<S2Edge> unusedEdges = Lists.newArrayList();
391       if (test.xorEdges < 0) {
392         builder.assembleLoops(loops, unusedEdges);
393       } else {
394         S2Polygon polygon = new S2Polygon();
395         builder.assemblePolygon(polygon, unusedEdges);
396         polygon.release(loops);
397       }
398       List<S2Loop> expected = Lists.newArrayList();
399       for (String loop : test.loopsOut) {
400         List<S2Point> vertices = Lists.newArrayList();
401         getVertices(loop, x, y, z, 0, vertices);
402         expected.add(new S2Loop(vertices));
403       }
404       // We assume that the vertex locations in the expected output polygon
405       // are separated from the corresponding vertex locations in the input
406       // edges by at most half of the minimum merge distance. Essentially
407       // this means that the expected output vertices should be near the
408       // centroid of the various input vertices.
409       double maxError = 0.5 * minMerge + maxPerturbation;
410 
411       // Note single "|" below so that we print both sets of loops.
412       if (findMissingLoops(loops, expected, maxError, "Actual")
413           | findMissingLoops(expected, loops, maxError, "Expected")) {
414         System.err.print(
415             "During iteration " + iter + ", undirected: " + options.getUndirectedEdges() + ", xor: "
416                 + options.getXorEdges() + "\n\n");
417         return false;
418       }
419       if (unusedEdges.size() != test.numUnusedEdges) {
420         System.err.print("Wrong number of unused edges: " + unusedEdges.size() + "%d (should be "
421             + test.numUnusedEdges + ")\n");
422         return false;
423       }
424     }
425     return true;
426   }
427 
testAssembleLoops()428   public void testAssembleLoops() {
429     boolean success = true;
430     for (int i = 0; i < testCases.length; ++i) {
431       log.info("Starting test case " + i);
432 
433       boolean caseSuccess = testBuilder(testCases[i]);
434 
435       log.info("Test case " + i + " finished: " + ((caseSuccess) ? "SUCCESS" : "FAILED"));
436 
437       success &= caseSuccess;
438     }
439     assertTrue(success);
440   }
441 }
442