From c135e9a7d6aedc317879a716716224b3b19c91a1 Mon Sep 17 00:00:00 2001 From: Robert Muir Date: Mon, 18 Apr 2016 00:16:56 -0400 Subject: [PATCH] LUCENE-7185: improve random test point/box generation for spatial tests --- .../org/apache/lucene/geo/TestGeoUtils.java | 78 +-- .../org/apache/lucene/geo/TestPolygon.java | 145 +++--- .../geopoint/search/TestGeoPointQuery.java | 20 - .../search/TestLegacyGeoPointQuery.java | 20 - .../lucene/geo/BaseGeoPointTestCase.java | 141 ++---- .../org/apache/lucene/geo/GeoTestUtil.java | 476 +++++++++++++----- 6 files changed, 491 insertions(+), 389 deletions(-) diff --git a/lucene/core/src/test/org/apache/lucene/geo/TestGeoUtils.java b/lucene/core/src/test/org/apache/lucene/geo/TestGeoUtils.java index 48db105b084..e75ae85b8a6 100644 --- a/lucene/core/src/test/org/apache/lucene/geo/TestGeoUtils.java +++ b/lucene/core/src/test/org/apache/lucene/geo/TestGeoUtils.java @@ -20,7 +20,6 @@ import java.util.Locale; import org.apache.lucene.util.LuceneTestCase; import org.apache.lucene.util.SloppyMath; -import org.junit.BeforeClass; /** * Tests class for methods in GeoUtils @@ -29,49 +28,16 @@ import org.junit.BeforeClass; */ public class TestGeoUtils extends LuceneTestCase { - // Global bounding box we will "cover" in the random test; we have to make this "smallish" else the queries take very long: - private static double originLat; - private static double originLon; - - @BeforeClass - public static void beforeClass() throws Exception { - originLon = GeoTestUtil.nextLongitude(); - originLat = GeoTestUtil.nextLatitude(); - } - - public double randomLat(boolean small) { - double result; - if (small) { - result = GeoTestUtil.nextLatitudeNear(originLat); - } else { - result = GeoTestUtil.nextLatitude(); - } - return result; - } - - public double randomLon(boolean small) { - double result; - if (small) { - result = GeoTestUtil.nextLongitudeNear(originLon); - } else { - result = GeoTestUtil.nextLongitude(); - } - return result; - } - // We rely heavily on GeoUtils.circleToBBox so we test it here: public void testRandomCircleToBBox() throws Exception { int iters = atLeast(1000); for(int iter=0;iter= box.minLat && lat <= box.maxLat); @@ -179,8 +130,9 @@ public class TestGeoUtils extends LuceneTestCase { SloppyMath.haversinSortKey(lat, lon, box.maxLat, lon)); for (int j = 0; j < 10000; j++) { - double lat2 = GeoTestUtil.nextLatitude(); - double lon2 = GeoTestUtil.nextLongitude(); + double point[] = GeoTestUtil.nextPointNear(box); + double lat2 = point[0]; + double lon2 = point[1]; // if the point is within radius, then it should be <= our sort key if (SloppyMath.haversinMeters(lat, lon, lat2, lon2) <= radius) { assertTrue(SloppyMath.haversinSortKey(lat, lon, lat2, lon2) <= minPartialDistance); diff --git a/lucene/core/src/test/org/apache/lucene/geo/TestPolygon.java b/lucene/core/src/test/org/apache/lucene/geo/TestPolygon.java index 86545ed6548..12c36900443 100644 --- a/lucene/core/src/test/org/apache/lucene/geo/TestPolygon.java +++ b/lucene/core/src/test/org/apache/lucene/geo/TestPolygon.java @@ -21,11 +21,12 @@ import org.apache.lucene.index.PointValues.Relation; import org.apache.lucene.util.LuceneTestCase; import static org.apache.lucene.geo.GeoTestUtil.nextLatitude; -import static org.apache.lucene.geo.GeoTestUtil.nextLatitudeAround; import static org.apache.lucene.geo.GeoTestUtil.nextLongitude; -import static org.apache.lucene.geo.GeoTestUtil.nextLongitudeAround; import static org.apache.lucene.geo.GeoTestUtil.nextPolygon; +import java.util.ArrayList; +import java.util.List; + public class TestPolygon extends LuceneTestCase { /** null polyLats not allowed */ @@ -124,13 +125,15 @@ public class TestPolygon extends LuceneTestCase { } } + // targets the bounding box directly public void testBoundingBoxEdgeCases() throws Exception { for (int i = 0; i < 100; i++) { Polygon polygon = nextPolygon(); for (int j = 0; j < 100; j++) { - double latitude = nextLatitudeAround(polygon.minLat, polygon.maxLat); - double longitude = nextLongitudeAround(polygon.minLon, polygon.maxLon); + double point[] = GeoTestUtil.nextPointNear(polygon); + double latitude = point[0]; + double longitude = point[1]; // if the point is within poly, then it should be in our bounding box if (polygon.contains(latitude, longitude)) { assertTrue(latitude >= polygon.minLat && latitude <= polygon.maxLat); @@ -146,13 +149,24 @@ public class TestPolygon extends LuceneTestCase { Polygon polygon = nextPolygon(); for (int j = 0; j < 100; j++) { - Rectangle rectangle = GeoTestUtil.nextSimpleBox(); + Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); // allowed to conservatively return false if (polygon.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == Relation.CELL_INSIDE_QUERY) { - for (int k = 0; k < 1000; k++) { + for (int k = 0; k < 500; k++) { // this tests in our range but sometimes outside! so we have to double-check its really in other box - double latitude = nextLatitudeAround(rectangle.minLat, rectangle.maxLat); - double longitude = nextLongitudeAround(rectangle.minLon, rectangle.maxLon); + double point[] = GeoTestUtil.nextPointNear(rectangle); + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertTrue(polygon.contains(latitude, longitude)); + } + } + for (int k = 0; k < 100; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(polygon); + double latitude = point[0]; + double longitude = point[1]; // check for sure its in our box if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { assertTrue(polygon.contains(latitude, longitude)); @@ -169,23 +183,29 @@ public class TestPolygon extends LuceneTestCase { public void testContainsEdgeCases() throws Exception { for (int i = 0; i < 1000; i++) { Polygon polygon = nextPolygon(); - - double polyLats[] = polygon.getPolyLats(); - double polyLons[] = polygon.getPolyLons(); - - for (int vertex = 0; vertex < polyLats.length; vertex++) { - for (int j = 0; j < 10; j++) { - Rectangle rectangle = GeoTestUtil.nextSimpleBoxNear(polyLats[vertex], polyLons[vertex]); - // allowed to conservatively return false - if (polygon.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == Relation.CELL_INSIDE_QUERY) { - for (int k = 0; k < 100; k++) { - // this tests in our range but sometimes outside! so we have to double-check its really in other box - double latitude = nextLatitudeAround(rectangle.minLat, rectangle.maxLat); - double longitude = nextLongitudeAround(rectangle.minLon, rectangle.maxLon); - // check for sure its in our box - if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { - assertTrue(polygon.contains(latitude, longitude)); - } + + for (int j = 0; j < 10; j++) { + Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); + // allowed to conservatively return false + if (polygon.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == Relation.CELL_INSIDE_QUERY) { + for (int k = 0; k < 100; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(rectangle); + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertTrue(polygon.contains(latitude, longitude)); + } + } + for (int k = 0; k < 20; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(polygon); + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertTrue(polygon.contains(latitude, longitude)); } } } @@ -199,13 +219,24 @@ public class TestPolygon extends LuceneTestCase { Polygon polygon = nextPolygon(); for (int j = 0; j < 100; j++) { - Rectangle rectangle = GeoTestUtil.nextSimpleBox(); + Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); // allowed to conservatively return true. if (polygon.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == Relation.CELL_OUTSIDE_QUERY) { for (int k = 0; k < 1000; k++) { + double point[] = GeoTestUtil.nextPointNear(rectangle); // this tests in our range but sometimes outside! so we have to double-check its really in other box - double latitude = nextLatitudeAround(rectangle.minLat, rectangle.maxLat); - double longitude = nextLongitudeAround(rectangle.minLon, rectangle.maxLon); + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertFalse(polygon.contains(latitude, longitude)); + } + } + for (int k = 0; k < 100; k++) { + double point[] = GeoTestUtil.nextPointNear(polygon); + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double latitude = point[0]; + double longitude = point[1]; // check for sure its in our box if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { assertFalse(polygon.contains(latitude, longitude)); @@ -223,22 +254,28 @@ public class TestPolygon extends LuceneTestCase { for (int i = 0; i < 100; i++) { Polygon polygon = nextPolygon(); - double polyLats[] = polygon.getPolyLats(); - double polyLons[] = polygon.getPolyLons(); - - for (int vertex = 0; vertex < polyLats.length; vertex++) { - for (int j = 0; j < 10; j++) { - Rectangle rectangle = GeoTestUtil.nextSimpleBoxNear(polyLats[vertex], polyLons[vertex]); - // allowed to conservatively return true. - if (polygon.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == Relation.CELL_OUTSIDE_QUERY) { - for (int k = 0; k < 100; k++) { - // this tests in our range but sometimes outside! so we have to double-check its really in other box - double latitude = nextLatitudeAround(rectangle.minLat, rectangle.maxLat); - double longitude = nextLongitudeAround(rectangle.minLon, rectangle.maxLon); - // check for sure its in our box - if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { - assertFalse(polygon.contains(latitude, longitude)); - } + for (int j = 0; j < 10; j++) { + Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); + // allowed to conservatively return false. + if (polygon.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == Relation.CELL_OUTSIDE_QUERY) { + for (int k = 0; k < 100; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(rectangle); + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertFalse(polygon.contains(latitude, longitude)); + } + } + for (int k = 0; k < 50; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(polygon); + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertFalse(polygon.contains(latitude, longitude)); } } } @@ -298,29 +335,17 @@ public class TestPolygon extends LuceneTestCase { double polyLats[] = polygon.getPolyLats(); double polyLons[] = polygon.getPolyLons(); - // random lat/lons in bounding box + // random lat/lons against polygon for (int j = 0; j < 1000; j++) { - double latitude = nextLatitudeAround(polygon.minLat, polygon.maxLat); - double longitude = nextLongitudeAround(polygon.minLon, polygon.maxLon); + double point[] = GeoTestUtil.nextPointNear(polygon); + double latitude = point[0]; + double longitude = point[1]; // bounding box check required due to rounding errors (we don't solve that problem) if (latitude >= polygon.minLat && latitude <= polygon.maxLat && longitude >= polygon.minLon && longitude <= polygon.maxLon) { boolean expected = containsOriginal(polyLats, polyLons, latitude, longitude); assertEquals(expected, polygon.contains(latitude, longitude)); } } - - // lat lons targeted near vertices - for (int vertex = 0; vertex < polyLats.length; vertex++) { - for (int j = 0; j < 100; j++) { - double latitude = GeoTestUtil.nextLatitudeNear(polyLats[vertex]); - double longitude = GeoTestUtil.nextLongitudeNear(polyLons[vertex]); - // bounding box check required due to rounding errors (we don't solve that problem) - if (latitude >= polygon.minLat && latitude <= polygon.maxLat && longitude >= polygon.minLon && longitude <= polygon.maxLon) { - boolean expected = containsOriginal(polyLats, polyLons, latitude, longitude); - assertEquals(expected, polygon.contains(latitude, longitude)); - } - } - } } } diff --git a/lucene/spatial/src/test/org/apache/lucene/spatial/geopoint/search/TestGeoPointQuery.java b/lucene/spatial/src/test/org/apache/lucene/spatial/geopoint/search/TestGeoPointQuery.java index e5b766daa64..ab6b6461f66 100644 --- a/lucene/spatial/src/test/org/apache/lucene/spatial/geopoint/search/TestGeoPointQuery.java +++ b/lucene/spatial/src/test/org/apache/lucene/spatial/geopoint/search/TestGeoPointQuery.java @@ -69,38 +69,18 @@ public class TestGeoPointQuery extends BaseGeoPointTestCase { return GeoPointTestUtil.nextLongitude(); } - @Override - protected double nextLongitudeNear(double other) { - return GeoPointTestUtil.nextLongitudeNear(other); - } - @Override protected double nextLatitude() { return GeoPointTestUtil.nextLatitude(); } - @Override - protected double nextLatitudeNear(double other) { - return GeoPointTestUtil.nextLatitudeNear(other); - } - @Override protected Rectangle nextBox() { return GeoPointTestUtil.nextBox(); } - @Override - protected Rectangle nextBoxNear(double latitude, double longitude) { - return GeoPointTestUtil.nextBoxNear(latitude, longitude); - } - @Override protected Polygon nextPolygon() { return GeoPointTestUtil.nextPolygon(); } - - @Override - protected Polygon nextPolygonNear(double latitude, double longitude) { - return GeoPointTestUtil.nextPolygonNear(latitude, longitude); - } } diff --git a/lucene/spatial/src/test/org/apache/lucene/spatial/geopoint/search/TestLegacyGeoPointQuery.java b/lucene/spatial/src/test/org/apache/lucene/spatial/geopoint/search/TestLegacyGeoPointQuery.java index d0b57fe9d4f..e56e28b0fd4 100644 --- a/lucene/spatial/src/test/org/apache/lucene/spatial/geopoint/search/TestLegacyGeoPointQuery.java +++ b/lucene/spatial/src/test/org/apache/lucene/spatial/geopoint/search/TestLegacyGeoPointQuery.java @@ -85,38 +85,18 @@ public class TestLegacyGeoPointQuery extends BaseGeoPointTestCase { return GeoPointTestUtil.nextLongitude(); } - @Override - protected double nextLongitudeNear(double other) { - return GeoPointTestUtil.nextLongitudeNear(other); - } - @Override protected double nextLatitude() { return GeoPointTestUtil.nextLatitude(); } - @Override - protected double nextLatitudeNear(double other) { - return GeoPointTestUtil.nextLatitudeNear(other); - } - @Override protected Rectangle nextBox() { return GeoPointTestUtil.nextBox(); } - @Override - protected Rectangle nextBoxNear(double latitude, double longitude) { - return GeoPointTestUtil.nextBoxNear(latitude, longitude); - } - @Override protected Polygon nextPolygon() { return GeoPointTestUtil.nextPolygon(); } - - @Override - protected Polygon nextPolygonNear(double latitude, double longitude) { - return GeoPointTestUtil.nextPolygonNear(latitude, longitude); - } } diff --git a/lucene/test-framework/src/java/org/apache/lucene/geo/BaseGeoPointTestCase.java b/lucene/test-framework/src/java/org/apache/lucene/geo/BaseGeoPointTestCase.java index dbdf189f3c4..6bc1e6e061e 100644 --- a/lucene/test-framework/src/java/org/apache/lucene/geo/BaseGeoPointTestCase.java +++ b/lucene/test-framework/src/java/org/apache/lucene/geo/BaseGeoPointTestCase.java @@ -83,16 +83,6 @@ import org.apache.lucene.util.bkd.BKDWriter; public abstract class BaseGeoPointTestCase extends LuceneTestCase { protected static final String FIELD_NAME = "point"; - - private double originLat; - private double originLon; - - @Override - public void setUp() throws Exception { - super.setUp(); - originLon = nextLongitude(); - originLat = nextLatitude(); - } // TODO: remove these hooks once all subclasses can pass with new random! @@ -100,34 +90,18 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { return org.apache.lucene.geo.GeoTestUtil.nextLongitude(); } - protected double nextLongitudeNear(double other) { - return org.apache.lucene.geo.GeoTestUtil.nextLongitudeNear(other); - } - protected double nextLatitude() { return org.apache.lucene.geo.GeoTestUtil.nextLatitude(); } - protected double nextLatitudeNear(double other) { - return org.apache.lucene.geo.GeoTestUtil.nextLatitudeNear(other); - } - protected Rectangle nextBox() { return org.apache.lucene.geo.GeoTestUtil.nextBox(); } - protected Rectangle nextBoxNear(double latitude, double longitude) { - return org.apache.lucene.geo.GeoTestUtil.nextBoxNear(latitude, longitude); - } - protected Polygon nextPolygon() { return org.apache.lucene.geo.GeoTestUtil.nextPolygon(); } - protected Polygon nextPolygonNear(double latitude, double longitude) { - return org.apache.lucene.geo.GeoTestUtil.nextPolygonNear(latitude, longitude); - } - /** Valid values that should not cause exception */ public void testIndexExtremeValues() { Document document = new Document(); @@ -418,11 +392,10 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { // A particularly tricky adversary for BKD tree: public void testSamePointManyTimes() throws Exception { int numPoints = atLeast(1000); - boolean small = random().nextBoolean(); // Every doc has 2 points: - double theLat = randomLat(small); - double theLon = randomLon(small); + double theLat = nextLatitude(); + double theLon = nextLongitude(); double[] lats = new double[numPoints]; Arrays.fill(lats, theLat); @@ -430,13 +403,12 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { double[] lons = new double[numPoints]; Arrays.fill(lons, theLon); - verify(small, lats, lons); + verify(lats, lons); } public void testAllLatEqual() throws Exception { int numPoints = atLeast(10000); - boolean small = random().nextBoolean(); - double lat = randomLat(small); + double lat = nextLatitude(); double[] lats = new double[numPoints]; double[] lons = new double[numPoints]; @@ -468,7 +440,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { System.out.println(" doc=" + docID + " lat=" + lat + " lon=" + lons[docID] + " (same lat/lon as doc=" + oldDocID + ")"); } } else { - lons[docID] = randomLon(small); + lons[docID] = nextLongitude(); haveRealDoc = true; if (VERBOSE) { System.out.println(" doc=" + docID + " lat=" + lat + " lon=" + lons[docID]); @@ -477,13 +449,12 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { lats[docID] = lat; } - verify(small, lats, lons); + verify(lats, lons); } public void testAllLonEqual() throws Exception { int numPoints = atLeast(10000); - boolean small = random().nextBoolean(); - double theLon = randomLon(small); + double theLon = nextLongitude(); double[] lats = new double[numPoints]; double[] lons = new double[numPoints]; @@ -517,7 +488,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { System.out.println(" doc=" + docID + " lat=" + lats[docID] + " lon=" + theLon + " (same lat/lon as doc=" + oldDocID + ")"); } } else { - lats[docID] = randomLat(small); + lats[docID] = nextLatitude(); haveRealDoc = true; if (VERBOSE) { System.out.println(" doc=" + docID + " lat=" + lats[docID] + " lon=" + theLon); @@ -526,7 +497,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { lons[docID] = theLon; } - verify(small, lats, lons); + verify(lats, lons); } public void testMultiValued() throws Exception { @@ -543,16 +514,14 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { iwc.setMergeScheduler(new SerialMergeScheduler()); RandomIndexWriter w = new RandomIndexWriter(random(), dir, iwc); - boolean small = random().nextBoolean(); - for (int id=0;id + * Don't pass huge numbers or infinity or anything like that yet. may have bugs! + */ + // the goal is to adjust random number generation to test edges, create more duplicates, create "one-offs" in floating point space, etc. + // we do this by first picking a good "base value" (explicitly targeting edges, zero if allowed, or "discrete values"). but it also + // ensures we pick any double in the range and generally still produces randomish looking numbers. + // then we sometimes perturb that by one ulp. + private static double nextDoubleInternal(double low, double high) { + assert low >= Integer.MIN_VALUE; + assert high <= Integer.MAX_VALUE; + assert Double.isFinite(low); + assert Double.isFinite(high); + assert high >= low : "low=" + low + " high=" + high; + + // if they are equal, not much we can do + if (low == high) { + return low; + } + + // first pick a base value. + final double baseValue; int surpriseMe = random().nextInt(17); if (surpriseMe == 0) { - // random bitpattern in range - return NumericUtils.sortableLongToDouble(TestUtil.nextLong(random(), LONGITUDE_MIN_SORTABLE, LONGITUDE_MAX_SORTABLE)); + // random bits + long lowBits = NumericUtils.doubleToSortableLong(low); + long highBits = NumericUtils.doubleToSortableLong(high); + baseValue = NumericUtils.sortableLongToDouble(TestUtil.nextLong(random(), lowBits, highBits)); } else if (surpriseMe == 1) { // edge case - return -180.0; + baseValue = low; } else if (surpriseMe == 2) { // edge case - return 180.0; - } else if (surpriseMe == 3) { + baseValue = high; + } else if (surpriseMe == 3 && low <= 0 && high >= 0) { // may trigger divide by 0 - return 0.0; + baseValue = 0.0; + } else if (surpriseMe == 4) { + // divide up space into block of 360 + double delta = (high - low) / 360; + int block = random().nextInt(360); + baseValue = low + delta * block; } else { // distributed ~ evenly - return -180 + 360.0 * random().nextDouble(); + baseValue = low + (high - low) * random().nextDouble(); + } + + assert baseValue >= low; + assert baseValue <= high; + + // either return the base value or adjust it by 1 ulp in a random direction (if possible) + int adjustMe = random().nextInt(17); + if (adjustMe == 0) { + return Math.nextAfter(adjustMe, high); + } else if (adjustMe == 1) { + return Math.nextAfter(adjustMe, low); + } else { + return baseValue; } } /** returns next pseudorandom latitude, kinda close to {@code otherLatitude} */ - public static double nextLatitudeNear(double otherLatitude) { + private static double nextLatitudeNear(double otherLatitude, double delta) { + delta = Math.abs(delta); GeoUtils.checkLatitude(otherLatitude); - return normalizeLatitude(otherLatitude + random().nextDouble() - 0.5); + int surpriseMe = random().nextInt(97); + if (surpriseMe == 0) { + // purely random + return nextLatitude(); + } else if (surpriseMe < 49) { + // upper half of region (the exact point or 1 ulp difference is still likely) + return nextDoubleInternal(otherLatitude, Math.min(90, otherLatitude + delta)); + } else { + // lower half of region (the exact point or 1 ulp difference is still likely) + return nextDoubleInternal(Math.max(-90, otherLatitude - delta), otherLatitude); + } } /** returns next pseudorandom longitude, kinda close to {@code otherLongitude} */ - public static double nextLongitudeNear(double otherLongitude) { + private static double nextLongitudeNear(double otherLongitude, double delta) { + delta = Math.abs(delta); GeoUtils.checkLongitude(otherLongitude); - return normalizeLongitude(otherLongitude + random().nextDouble() - 0.5); + int surpriseMe = random().nextInt(97); + if (surpriseMe == 0) { + // purely random + return nextLongitude(); + } else if (surpriseMe < 49) { + // upper half of region (the exact point or 1 ulp difference is still likely) + return nextDoubleInternal(otherLongitude, Math.min(180, otherLongitude + delta)); + } else { + // lower half of region (the exact point or 1 ulp difference is still likely) + return nextDoubleInternal(Math.max(-180, otherLongitude - delta), otherLongitude); + } } /** * returns next pseudorandom latitude, kinda close to {@code minLatitude/maxLatitude} * NOTE:minLatitude/maxLatitude are merely guidelines. the returned value is sometimes - * outside of that range! this is to facilitate edge testing. + * outside of that range! this is to facilitate edge testing of lines */ - public static double nextLatitudeAround(double minLatitude, double maxLatitude) { + private static double nextLatitudeBetween(double minLatitude, double maxLatitude) { + assert maxLatitude >= minLatitude; GeoUtils.checkLatitude(minLatitude); GeoUtils.checkLatitude(maxLatitude); - return normalizeLatitude(randomRangeMaybeSlightlyOutside(minLatitude, maxLatitude)); + if (random().nextInt(47) == 0) { + // purely random + return nextLatitude(); + } else { + // extend the range by 1% + double difference = (maxLatitude - minLatitude) / 100; + double lower = Math.max(-90, minLatitude - difference); + double upper = Math.min(90, maxLatitude + difference); + return nextDoubleInternal(lower, upper); + } } /** * returns next pseudorandom longitude, kinda close to {@code minLongitude/maxLongitude} * NOTE:minLongitude/maxLongitude are merely guidelines. the returned value is sometimes - * outside of that range! this is to facilitate edge testing. + * outside of that range! this is to facilitate edge testing of lines */ - public static double nextLongitudeAround(double minLongitude, double maxLongitude) { + private static double nextLongitudeBetween(double minLongitude, double maxLongitude) { + assert maxLongitude >= minLongitude; GeoUtils.checkLongitude(minLongitude); GeoUtils.checkLongitude(maxLongitude); - return normalizeLongitude(randomRangeMaybeSlightlyOutside(minLongitude, maxLongitude)); + if (random().nextInt(47) == 0) { + // purely random + return nextLongitude(); + } else { + // extend the range by 1% + double difference = (maxLongitude - minLongitude) / 100; + double lower = Math.max(-180, minLongitude - difference); + double upper = Math.min(180, maxLongitude + difference); + return nextDoubleInternal(lower, upper); + } + } + + /** Returns the next point around a line (more or less) */ + private static double[] nextPointAroundLine(double lat1, double lon1, double lat2, double lon2) { + double x1 = lon1; + double x2 = lon2; + double y1 = lat1; + double y2 = lat2; + double minX = Math.min(x1, x2); + double maxX = Math.max(x1, x2); + double minY = Math.min(y1, y2); + double maxY = Math.max(y1, y2); + if (minX == maxX) { + return new double[] { nextLatitudeBetween(minY, maxY), nextLongitudeNear(minX, 0.01 * (maxY - minY)) }; + } else if (minY == maxY) { + return new double[] { nextLatitudeNear(minY, 0.01 * (maxX - minX)), nextLongitudeBetween(minX, maxX) }; + } else { + double x = nextLongitudeBetween(minX, maxX); + double y = (y1 - y2) / (x1 - x2) * (x-x1) + y1; + double delta = (maxY - minY) * 0.01; + // our formula may put the targeted Y out of bounds + y = Math.min(90, y); + y = Math.max(-90, y); + return new double[] { nextLatitudeNear(y, delta), x }; + } + } + + /** Returns next point (lat/lon) for testing near a Box. It may cross the dateline */ + public static double[] nextPointNear(Rectangle rectangle) { + if (rectangle.crossesDateline()) { + // pick a "side" of the two boxes we really are + if (random().nextBoolean()) { + return nextPointNear(new Rectangle(rectangle.minLat, rectangle.maxLat, -180, rectangle.maxLon)); + } else { + return nextPointNear(new Rectangle(rectangle.minLat, rectangle.maxLat, rectangle.minLon, 180)); + } + } else { + return nextPointNear(boxPolygon(rectangle)); + } + } + + /** Returns next point (lat/lon) for testing near a Polygon */ + // see http://www-ma2.upc.es/geoc/Schirra-pointPolygon.pdf for more info on some of these strategies + public static double[] nextPointNear(Polygon polygon) { + double polyLats[] = polygon.getPolyLats(); + double polyLons[] = polygon.getPolyLons(); + Polygon holes[] = polygon.getHoles(); + + // if there are any holes, target them aggressively + if (holes.length > 0 && random().nextInt(3) == 0) { + return nextPointNear(holes[random().nextInt(holes.length)]); + } + + int surpriseMe = random().nextInt(97); + if (surpriseMe == 0) { + // purely random + return new double[] { nextLatitude(), nextLongitude() }; + } else if (surpriseMe < 5) { + // purely random within bounding box + return new double[] { nextLatitudeBetween(polygon.minLat, polygon.maxLat), nextLongitudeBetween(polygon.minLon, polygon.maxLon) }; + } else if (surpriseMe < 20) { + // target a vertex + int vertex = random().nextInt(polyLats.length - 1); + return new double[] { nextLatitudeNear(polyLats[vertex], polyLats[vertex+1] - polyLats[vertex]), + nextLongitudeNear(polyLons[vertex], polyLons[vertex+1] - polyLons[vertex]) }; + } else if (surpriseMe < 30) { + // target points around the bounding box edges + Polygon container = boxPolygon(new Rectangle(polygon.minLat, polygon.maxLat, polygon.minLon, polygon.maxLon)); + double containerLats[] = container.getPolyLats(); + double containerLons[] = container.getPolyLons(); + int startVertex = random().nextInt(containerLats.length - 1); + return nextPointAroundLine(containerLats[startVertex], containerLons[startVertex], + containerLats[startVertex+1], containerLons[startVertex+1]); + } else { + // target points around diagonals between vertices + int startVertex = random().nextInt(polyLats.length - 1); + // but favor edges heavily + int endVertex = random().nextBoolean() ? startVertex + 1 : random().nextInt(polyLats.length - 1); + return nextPointAroundLine(polyLats[startVertex], polyLons[startVertex], + polyLats[endVertex], polyLons[endVertex]); + } + } + + /** Returns next box for testing near a Polygon */ + public static Rectangle nextBoxNear(Polygon polygon) { + final double point1[]; + final double point2[]; + + // if there are any holes, target them aggressively + Polygon holes[] = polygon.getHoles(); + if (holes.length > 0 && random().nextInt(3) == 0) { + return nextBoxNear(holes[random().nextInt(holes.length)]); + } + + int surpriseMe = random().nextInt(97); + if (surpriseMe == 0) { + // formed from two interesting points + point1 = nextPointNear(polygon); + point2 = nextPointNear(polygon); + } else { + // formed from one interesting point: then random within delta. + point1 = nextPointNear(polygon); + point2 = new double[2]; + // now figure out a good delta: we use a rough heuristic, up to the length of an edge + double polyLats[] = polygon.getPolyLats(); + double polyLons[] = polygon.getPolyLons(); + int vertex = random().nextInt(polyLats.length - 1); + double deltaX = polyLons[vertex+1] - polyLons[vertex]; + double deltaY = polyLats[vertex+1] - polyLats[vertex]; + double edgeLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + point2[0] = nextLatitudeNear(point1[0], edgeLength); + point2[1] = nextLongitudeNear(point1[1], edgeLength); + } + + // form a box from the two points + double minLat = Math.min(point1[0], point2[0]); + double maxLat = Math.max(point1[0], point2[0]); + double minLon = Math.min(point1[1], point2[1]); + double maxLon = Math.max(point1[1], point2[1]); + return new Rectangle(minLat, maxLat, minLon, maxLon); } /** returns next pseudorandom box: can cross the 180th meridian */ public static Rectangle nextBox() { return nextBoxInternal(nextLatitude(), nextLatitude(), nextLongitude(), nextLongitude(), true); } - - /** returns next pseudorandom box: will not cross the 180th meridian */ - public static Rectangle nextSimpleBox() { - return nextBoxInternal(nextLatitude(), nextLatitude(), nextLongitude(), nextLongitude(), false); - } - - /** returns next pseudorandom box, can cross the 180th meridian, kinda close to {@code otherLatitude} and {@code otherLongitude} */ - public static Rectangle nextBoxNear(double otherLatitude, double otherLongitude) { - GeoUtils.checkLongitude(otherLongitude); - GeoUtils.checkLongitude(otherLongitude); - return nextBoxInternal(nextLatitudeNear(otherLatitude), nextLatitudeNear(otherLatitude), - nextLongitudeNear(otherLongitude), nextLongitudeNear(otherLongitude), true); - } - - /** returns next pseudorandom box, will not cross the 180th meridian, kinda close to {@code otherLatitude} and {@code otherLongitude} */ - public static Rectangle nextSimpleBoxNear(double otherLatitude, double otherLongitude) { - GeoUtils.checkLongitude(otherLongitude); - GeoUtils.checkLongitude(otherLongitude); - return nextBoxInternal(nextLatitudeNear(otherLatitude), nextLatitudeNear(otherLatitude), - nextLongitudeNear(otherLongitude), nextLongitudeNear(otherLongitude), false); - } /** Makes an n-gon, centered at the provided lat/lon, and each vertex approximately * distanceMeters away from the center. @@ -211,7 +378,7 @@ public class GeoTestUtil { /** returns next pseudorandom polygon */ public static Polygon nextPolygon() { if (random().nextBoolean()) { - return surpriseMePolygon(null, null); + return surpriseMePolygon(); } else if (random().nextInt(10) == 1) { // this poly is slow to create ... only do it 10% of the time: while (true) { @@ -236,23 +403,6 @@ public class GeoTestUtil { } } - /** returns next pseudorandom polygon, kinda close to {@code otherLatitude} and {@code otherLongitude} */ - public static Polygon nextPolygonNear(double otherLatitude, double otherLongitude) { - if (random().nextBoolean()) { - return surpriseMePolygon(otherLatitude, otherLongitude); - } - - Rectangle box = nextBoxInternal(nextLatitudeNear(otherLatitude), nextLatitudeNear(otherLatitude), - nextLongitudeNear(otherLongitude), nextLongitudeNear(otherLongitude), false); - if (random().nextBoolean()) { - // box - return boxPolygon(box); - } else { - // triangle - return trianglePolygon(box); - } - } - private static Rectangle nextBoxInternal(double lat0, double lat1, double lon0, double lon1, boolean canCrossDateLine) { if (lat1 < lat0) { double x = lat0; @@ -301,23 +451,13 @@ public class GeoTestUtil { return new Polygon(polyLats, polyLons); } - private static Polygon surpriseMePolygon(Double otherLatitude, Double otherLongitude) { + private static Polygon surpriseMePolygon() { // repeat until we get a poly that doesn't cross dateline: newPoly: while (true) { //System.out.println("\nPOLY ITER"); - final double centerLat; - final double centerLon; - if (otherLatitude == null) { - centerLat = nextLatitude(); - centerLon = nextLongitude(); - } else { - GeoUtils.checkLatitude(otherLatitude); - GeoUtils.checkLongitude(otherLongitude); - centerLat = nextLatitudeNear(otherLatitude); - centerLon = nextLongitudeNear(otherLongitude); - } - + double centerLat = nextLatitude(); + double centerLon = nextLongitude(); double radius = 0.1 + 20 * random().nextDouble(); double radiusDelta = random().nextDouble(); @@ -371,37 +511,131 @@ public class GeoTestUtil { } } - /** Returns random double min to max or up to 1% outside of that range */ - private static double randomRangeMaybeSlightlyOutside(double min, double max) { - return min + (random().nextDouble() + (0.5 - random().nextDouble()) * .02) * (max - min); - } - - /** Puts latitude in range of -90 to 90. */ - private static double normalizeLatitude(double latitude) { - if (latitude >= -90 && latitude <= 90) { - return latitude; //common case, and avoids slight double precision shifting - } - double off = Math.abs((latitude + 90) % 360); - return (off <= 180 ? off : 360-off) - 90; - } - - /** Puts longitude in range of -180 to +180. */ - private static double normalizeLongitude(double longitude) { - if (longitude >= -180 && longitude <= 180) { - return longitude; //common case, and avoids slight double precision shifting - } - double off = (longitude + 180) % 360; - if (off < 0) { - return 180 + off; - } else if (off == 0 && longitude > 0) { - return 180; - } else { - return -180 + off; - } - } - /** Keep it simple, we don't need to take arbitrary Random for geo tests */ private static Random random() { return RandomizedContext.current().getRandom(); } + + /** + * Returns svg of polygon for debugging. + *

+ * You can pass any number of objects: + * Polygon: polygon with optional holes + * Polygon[]: arrays of polygons for convenience + * Rectangle: for a box + * double[2]: as latitude,longitude for a point + *

+ * At least one object must be a polygon. The viewBox is formed around all polygons + * found in the arguments. + */ + public static String toSVG(Object ...objects) { + List flattened = new ArrayList<>(); + for (Object o : objects) { + if (o instanceof Polygon[]) { + flattened.addAll(Arrays.asList((Polygon[]) o)); + } else { + flattened.add(o); + } + } + // first compute bounding area of all the objects + double minLat = Double.POSITIVE_INFINITY; + double maxLat = Double.NEGATIVE_INFINITY; + double minLon = Double.POSITIVE_INFINITY; + double maxLon = Double.NEGATIVE_INFINITY; + for (Object o : flattened) { + final Rectangle r; + if (o instanceof Polygon) { + r = Rectangle.fromPolygon(new Polygon[] { (Polygon) o }); + minLat = Math.min(minLat, r.minLat); + maxLat = Math.max(maxLat, r.maxLat); + minLon = Math.min(minLon, r.minLon); + maxLon = Math.max(maxLon, r.maxLon); + } + } + if (Double.isFinite(minLat) == false || Double.isFinite(maxLat) == false || + Double.isFinite(minLon) == false || Double.isFinite(maxLon) == false) { + throw new IllegalArgumentException("you must pass at least one polygon"); + } + + // add some additional padding so we can really see what happens on the edges too + double xpadding = (maxLon - minLon) / 64; + double ypadding = (maxLat - minLat) / 64; + // expand points to be this large + double pointX = xpadding * 0.1; + double pointY = ypadding * 0.1; + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + + // encode each object + for (Object o : flattened) { + // tostring + if (o instanceof double[]) { + double point[] = (double[]) o; + sb.append("\n"); + } else { + sb.append("\n"); + } + final Polygon gon; + final String style; + final String opacity; + if (o instanceof Rectangle) { + gon = boxPolygon((Rectangle) o); + style = "fill:lightskyblue;stroke:black;stroke-width:0.2%;stroke-dasharray:0.5%,1%;"; + opacity = "0.3"; + } else if (o instanceof double[]) { + double point[] = (double[]) o; + gon = boxPolygon(new Rectangle(Math.max(-90, point[0]-pointY), + Math.min(90, point[0]+pointY), + Math.max(-180, point[1]-pointX), + Math.min(180, point[1]+pointX))); + style = "fill:red;stroke:red;stroke-width:0.1%;"; + opacity = "0.7"; + } else { + gon = (Polygon) o; + style = "fill:lawngreen;stroke:black;stroke-width:0.3%;"; + opacity = "0.5"; + } + // polygon + double polyLats[] = gon.getPolyLats(); + double polyLons[] = gon.getPolyLons(); + sb.append(" 0) { + sb.append(" "); + } + sb.append(polyLons[i]) + .append(",") + .append(90 - polyLats[i]); + } + sb.append("\" style=\"" + style + "\"/>\n"); + for (Polygon hole : gon.getHoles()) { + double holeLats[] = hole.getPolyLats(); + double holeLons[] = hole.getPolyLons(); + sb.append(" 0) { + sb.append(" "); + } + sb.append(holeLons[i]) + .append(",") + .append(90 - holeLats[i]); + } + sb.append("\" style=\"fill:lightgray\"/>\n"); + } + } + sb.append("\n"); + return sb.toString(); + } }