You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by rm...@apache.org on 2016/04/18 06:38:54 UTC

lucene-solr:branch_6x: LUCENE-7185: improve random test point/box generation for spatial tests

Repository: lucene-solr
Updated Branches:
  refs/heads/branch_6x a0221f469 -> 2138bc053


LUCENE-7185: improve random test point/box generation for spatial tests


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/2138bc05
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/2138bc05
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/2138bc05

Branch: refs/heads/branch_6x
Commit: 2138bc05365e362dd5aa8df4224fd853e981de2f
Parents: a0221f4
Author: Robert Muir <rm...@apache.org>
Authored: Mon Apr 18 00:16:56 2016 -0400
Committer: Robert Muir <rm...@apache.org>
Committed: Mon Apr 18 00:19:28 2016 -0400

----------------------------------------------------------------------
 .../org/apache/lucene/geo/TestGeoUtils.java     |  78 +---
 .../test/org/apache/lucene/geo/TestPolygon.java | 145 +++---
 .../geopoint/search/TestGeoPointQuery.java      |  20 -
 .../search/TestLegacyGeoPointQuery.java         |  20 -
 .../apache/lucene/geo/BaseGeoPointTestCase.java | 141 ++----
 .../java/org/apache/lucene/geo/GeoTestUtil.java | 468 ++++++++++++++-----
 6 files changed, 487 insertions(+), 385 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2138bc05/lucene/core/src/test/org/apache/lucene/geo/TestGeoUtils.java
----------------------------------------------------------------------
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 48db105..e75ae85 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<iters;iter++) {
 
-      boolean useSmallRanges = random().nextBoolean();
-
-      double radiusMeters;
-
-      double centerLat = randomLat(useSmallRanges);
-      double centerLon = randomLon(useSmallRanges);
+      double centerLat = GeoTestUtil.nextLatitude();
+      double centerLon = GeoTestUtil.nextLongitude();
 
-      if (useSmallRanges) {
+      final double radiusMeters;
+      if (random().nextBoolean()) {
         // Approx 4 degrees lon at the equator:
         radiusMeters = random().nextDouble() * 444000;
       } else {
@@ -85,25 +51,9 @@ public class TestGeoUtils extends LuceneTestCase {
       int numPointsToTry = 1000;
       for(int i=0;i<numPointsToTry;i++) {
 
-        double lat;
-        double lon;
-
-        if (random().nextBoolean()) {
-          lat = randomLat(useSmallRanges);
-          lon = randomLon(useSmallRanges);
-        } else {
-          // pick a lat/lon within the bbox or "slightly" outside it to try to improve test efficiency
-          lat = GeoTestUtil.nextLatitudeAround(bbox.minLat, bbox.maxLat);
-          if (bbox.crossesDateline()) {
-            if (random().nextBoolean()) {
-              lon = GeoTestUtil.nextLongitudeAround(bbox.maxLon, -180);
-            } else {
-              lon = GeoTestUtil.nextLongitudeAround(0, bbox.minLon);
-            }
-          } else {
-            lon = GeoTestUtil.nextLongitudeAround(bbox.minLon, bbox.maxLon);
-          }
-        }
+        double point[] = GeoTestUtil.nextPointNear(bbox);
+        double lat = point[0];
+        double lon = point[1];
 
         double distanceMeters = SloppyMath.haversinMeters(centerLat, centerLon, lat, lon);
 
@@ -124,7 +74,7 @@ public class TestGeoUtils extends LuceneTestCase {
 
         if (haversinSays) {
           if (bboxSays == false) {
-            System.out.println("small=" + useSmallRanges + " centerLat=" + centerLat + " cetnerLon=" + centerLon + " radiusMeters=" + radiusMeters);
+            System.out.println("centerLat=" + centerLat + " centerLon=" + centerLon + " radiusMeters=" + radiusMeters);
             System.out.println("  bbox: lat=" + bbox.minLat + " to " + bbox.maxLat + " lon=" + bbox.minLon + " to " + bbox.maxLon);
             System.out.println("  point: lat=" + lat + " lon=" + lon);
             System.out.println("  haversin: " + distanceMeters);
@@ -154,9 +104,10 @@ public class TestGeoUtils extends LuceneTestCase {
         box2 = null;
       }
 
-      for (int j = 0; j < 10000; j++) {
-        double lat2 = GeoTestUtil.nextLatitude();
-        double lon2 = GeoTestUtil.nextLongitude();
+      for (int j = 0; j < 1000; j++) {
+        double point[] = GeoTestUtil.nextPointNear(box);
+        double lat2 = point[0];
+        double lon2 = point[1];
         // if the point is within radius, then it should be in our bounding box
         if (SloppyMath.haversinMeters(lat, lon, lat2, lon2) <= radius) {
           assertTrue(lat >= 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);

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2138bc05/lucene/core/src/test/org/apache/lucene/geo/TestPolygon.java
----------------------------------------------------------------------
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 86545ed..12c3690 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));
-          }
-        }
-      }
     }
   }
   

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2138bc05/lucene/spatial/src/test/org/apache/lucene/spatial/geopoint/search/TestGeoPointQuery.java
----------------------------------------------------------------------
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 e5b766d..ab6b646 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
@@ -70,37 +70,17 @@ public class TestGeoPointQuery extends BaseGeoPointTestCase {
   }
 
   @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);
-  }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2138bc05/lucene/spatial/src/test/org/apache/lucene/spatial/geopoint/search/TestLegacyGeoPointQuery.java
----------------------------------------------------------------------
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 d0b57fe..e56e28b 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
@@ -86,37 +86,17 @@ public class TestLegacyGeoPointQuery extends BaseGeoPointTestCase {
   }
 
   @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);
-  }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2138bc05/lucene/test-framework/src/java/org/apache/lucene/geo/BaseGeoPointTestCase.java
----------------------------------------------------------------------
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 dbdf189..6bc1e6e 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<numPoints;id++) {
       Document doc = new Document();
-      lats[2*id] = quantizeLat(randomLat(small));
-      lons[2*id] = quantizeLon(randomLon(small));
+      lats[2*id] = quantizeLat(nextLatitude());
+      lons[2*id] = quantizeLon(nextLongitude());
       doc.add(newStringField("id", ""+id, Field.Store.YES));
       addPointToDoc(FIELD_NAME, doc, lats[2*id], lons[2*id]);
-      lats[2*id+1] = quantizeLat(randomLat(small));
-      lons[2*id+1] = quantizeLon(randomLon(small));
+      lats[2*id+1] = quantizeLat(nextLatitude());
+      lons[2*id+1] = quantizeLon(nextLongitude());
       addPointToDoc(FIELD_NAME, doc, lats[2*id+1], lons[2*id+1]);
 
       if (VERBOSE) {
@@ -574,7 +543,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
 
     int iters = atLeast(25);
     for (int iter=0;iter<iters;iter++) {
-      Rectangle rect = randomRect(small);
+      Rectangle rect = nextBox();
 
       if (VERBOSE) {
         System.out.println("\nTEST: iter=" + iter + " rect=" + rect);
@@ -665,8 +634,6 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
     double[] lats = new double[numPoints];
     double[] lons = new double[numPoints];
 
-    boolean small = random().nextBoolean();
-
     boolean haveRealDoc = false;
 
     for (int id=0;id<numPoints;id++) {
@@ -692,13 +659,13 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
         if (x == 0) {
           // Identical lat to old point
           lats[id] = lats[oldID];
-          lons[id] = randomLon(small);
+          lons[id] = nextLongitude();
           if (VERBOSE) {
             System.out.println("  id=" + id + " lat=" + lats[id] + " lon=" + lons[id] + " (same lat as doc=" + oldID + ")");
           }
         } else if (x == 1) {
           // Identical lon to old point
-          lats[id] = randomLat(small);
+          lats[id] = nextLatitude();
           lons[id] = lons[oldID];
           if (VERBOSE) {
             System.out.println("  id=" + id + " lat=" + lats[id] + " lon=" + lons[id] + " (same lon as doc=" + oldID + ")");
@@ -713,8 +680,8 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
           }
         }
       } else {
-        lats[id] = randomLat(small);
-        lons[id] = randomLon(small);
+        lats[id] = nextLatitude();
+        lons[id] = nextLongitude();
         haveRealDoc = true;
         if (VERBOSE) {
           System.out.println("  id=" + id + " lat=" + lats[id] + " lon=" + lons[id]);
@@ -722,23 +689,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
       }
     }
 
-    verify(small, lats, lons);
-  }
-
-  public final double randomLat(boolean small) {
-    if (small) {
-      return nextLatitudeNear(originLat);
-    } else {
-      return nextLatitude();
-    }
-  }
-
-  public final double randomLon(boolean small) {
-    if (small) {
-      return nextLongitudeNear(originLon);
-    } else {
-      return nextLongitude();
-    }
+    verify(lats, lons);
   }
 
   /** Override this to quantize randomly generated lat, so the test won't fail due to quantization errors, which are 1) annoying to debug,
@@ -753,14 +704,6 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
     return lon;
   }
 
-  protected final Rectangle randomRect(boolean small) {
-    if (small) {
-      return nextBoxNear(originLat, originLon);
-    } else {
-      return nextBox();
-    }
-  }
-
   protected abstract void addPointToDoc(String field, Document doc, double lat, double lon);
 
   protected abstract Query newRectQuery(String field, double minLat, double maxLat, double minLon, double maxLon);
@@ -784,7 +727,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
     }
   }
 
-  private void verify(boolean small, double[] lats, double[] lons) throws Exception {
+  private void verify(double[] lats, double[] lons) throws Exception {
     // quantize each value the same way the index does
     // NaN means missing for the doc!!!!!
     for (int i = 0; i < lats.length; i++) {
@@ -797,12 +740,12 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
         lons[i] = quantizeLon(lons[i]);
       }
     }
-    verifyRandomRectangles(small, lats, lons);
-    verifyRandomDistances(small, lats, lons);
-    verifyRandomPolygons(small, lats, lons);
+    verifyRandomRectangles(lats, lons);
+    verifyRandomDistances(lats, lons);
+    verifyRandomPolygons(lats, lons);
   }
 
-  protected void verifyRandomRectangles(boolean small, double[] lats, double[] lons) throws Exception {
+  protected void verifyRandomRectangles(double[] lats, double[] lons) throws Exception {
     IndexWriterConfig iwc = newIndexWriterConfig();
     // Else seeds may not reproduce:
     iwc.setMergeScheduler(new SerialMergeScheduler());
@@ -860,7 +803,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
         System.out.println("\nTEST: iter=" + iter + " s=" + s);
       }
       
-      Rectangle rect = randomRect(small);
+      Rectangle rect = nextBox();
 
       Query query = newRectQuery(FIELD_NAME, rect.minLat, rect.maxLat, rect.minLon, rect.maxLon);
 
@@ -930,7 +873,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
     IOUtils.close(r, dir);
   }
 
-  protected void verifyRandomDistances(boolean small, double[] lats, double[] lons) throws Exception {
+  protected void verifyRandomDistances(double[] lats, double[] lons) throws Exception {
     IndexWriterConfig iwc = newIndexWriterConfig();
     // Else seeds may not reproduce:
     iwc.setMergeScheduler(new SerialMergeScheduler());
@@ -989,17 +932,11 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
       }
 
       // Distance
-      final double centerLat = randomLat(small);
-      final double centerLon = randomLon(small);
+      final double centerLat = nextLatitude();
+      final double centerLon = nextLongitude();
 
-      final double radiusMeters;
-      if (small) {
-        // Approx 3 degrees lon at the equator:
-        radiusMeters = random().nextDouble() * 333000 + 1.0;
-      } else {
-        // So the query can cover at most 50% of the earth's surface:
-        radiusMeters = random().nextDouble() * GeoUtils.EARTH_MEAN_RADIUS_METERS * Math.PI / 2.0 + 1.0;
-      }
+      // So the query can cover at most 50% of the earth's surface:
+      final double radiusMeters = random().nextDouble() * GeoUtils.EARTH_MEAN_RADIUS_METERS * Math.PI / 2.0 + 1.0;
 
       if (VERBOSE) {
         final DecimalFormat df = new DecimalFormat("#,###.00", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
@@ -1077,7 +1014,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
     IOUtils.close(r, dir);
   }
 
-  protected void verifyRandomPolygons(boolean small, double[] lats, double[] lons) throws Exception {
+  protected void verifyRandomPolygons(double[] lats, double[] lons) throws Exception {
     IndexWriterConfig iwc = newIndexWriterConfig();
     // Else seeds may not reproduce:
     iwc.setMergeScheduler(new SerialMergeScheduler());
@@ -1137,13 +1074,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
       }
 
       // Polygon
-      final Polygon polygon;
-      if (small) {
-        polygon = nextPolygonNear(originLat, originLon);
-      } else {
-        polygon = nextPolygon();
-      }
-      
+      Polygon polygon = nextPolygon();
       Query query = newPolygonQuery(FIELD_NAME, polygon);
 
       if (VERBOSE) {
@@ -1216,7 +1147,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
     Rectangle rect;
     // TODO: why this dateline leniency???
     while (true) {
-      rect = randomRect(random().nextBoolean());
+      rect = nextBox();
       if (rect.crossesDateline() == false) {
         break;
       }
@@ -1386,7 +1317,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
   public void testEquals() throws Exception {   
     Query q1, q2;
 
-    Rectangle rect = randomRect(false);
+    Rectangle rect = nextBox();
 
     q1 = newRectQuery("field", rect.minLat, rect.maxLat, rect.minLon, rect.maxLon);
     q2 = newRectQuery("field", rect.minLat, rect.maxLat, rect.minLon, rect.maxLon);
@@ -1397,8 +1328,8 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
       assertFalse(q1.equals(newRectQuery("field2", rect.minLat, rect.maxLat, rect.minLon, rect.maxLon)));
     }
 
-    double lat = randomLat(false);
-    double lon = randomLon(false);
+    double lat = nextLatitude();
+    double lon = nextLongitude();
     q1 = newDistanceQuery("field", lat, lon, 10000.0);
     q2 = newDistanceQuery("field", lat, lon, 10000.0);
     assertEquals(q1, q2);

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/2138bc05/lucene/test-framework/src/java/org/apache/lucene/geo/GeoTestUtil.java
----------------------------------------------------------------------
diff --git a/lucene/test-framework/src/java/org/apache/lucene/geo/GeoTestUtil.java b/lucene/test-framework/src/java/org/apache/lucene/geo/GeoTestUtil.java
index f7d4eae..eb48a8f 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/geo/GeoTestUtil.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/geo/GeoTestUtil.java
@@ -17,6 +17,8 @@
 package org.apache.lucene.geo;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.Random;
 
 import org.apache.lucene.util.NumericUtils;
@@ -28,112 +30,277 @@ import com.carrotsearch.randomizedtesting.RandomizedContext;
 /** static methods for testing geo */
 public class GeoTestUtil {
 
-  private static final long LATITUDE_MIN_SORTABLE = NumericUtils.doubleToSortableLong(-90);
-  private static final long LATITUDE_MAX_SORTABLE = NumericUtils.doubleToSortableLong(90);
-
   /** returns next pseudorandom latitude (anywhere) */
   public static double nextLatitude() {
-    int surpriseMe = random().nextInt(17);
-    if (surpriseMe == 0) {
-      // random bitpattern in range
-      return NumericUtils.sortableLongToDouble(TestUtil.nextLong(random(), LATITUDE_MIN_SORTABLE, LATITUDE_MAX_SORTABLE));
-    } else if (surpriseMe == 1) {
-      // edge case
-      return -90.0;
-    } else if (surpriseMe == 2) {
-      // edge case
-      return 90.0;
-    } else if (surpriseMe == 3) {
-      // may trigger divide by zero
-      return 0.0;
-    } else {
-      // distributed ~ evenly
-      return -90 + 180.0 * random().nextDouble();
-    }
+    return nextDoubleInternal(-90, 90);
   }
 
-  private static final long LONGITUDE_MIN_SORTABLE = NumericUtils.doubleToSortableLong(-180);
-  private static final long LONGITUDE_MAX_SORTABLE = NumericUtils.doubleToSortableLong(180);
-
   /** returns next pseudorandom longitude (anywhere) */
   public static double nextLongitude() {
+    return nextDoubleInternal(-180, 180);
+  }
+  
+  /**
+   * Returns next double within range.
+   * <p>
+   * 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}
    * <b>NOTE:</b>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}
    * <b>NOTE:</b>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 next pseudorandom box: can cross the 180th meridian */
-  public static Rectangle nextBox() {
-    return nextBoxInternal(nextLatitude(), nextLatitude(), nextLongitude(), nextLongitude(), true);
+  
+  /** 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 pseudorandom box: will not cross the 180th meridian */
-  public static Rectangle nextSimpleBox() {
-    return nextBoxInternal(nextLatitude(), nextLatitude(), nextLongitude(), nextLongitude(), false);
+  /** 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 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 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 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);
+  /** 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);
   }
 
   /** Makes an n-gon, centered at the provided lat/lon, and each vertex approximately
@@ -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);
+  /** Keep it simple, we don't need to take arbitrary Random for geo tests */
+  private static Random random() {
+   return RandomizedContext.current().getRandom();
   }
 
-  /** 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
+  /** 
+   * Returns svg of polygon for debugging. 
+   * <p>
+   * 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
+   * <p>
+   * 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<Object> flattened = new ArrayList<>();
+    for (Object o : objects) {
+      if (o instanceof Polygon[]) {
+        flattened.addAll(Arrays.asList((Polygon[]) o));
+      } else {
+        flattened.add(o);
+      }
     }
-    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
+    // 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);
+      }
     }
-    double off = (longitude + 180) % 360;
-    if (off < 0) {
-      return 180 + off;
-    } else if (off == 0 && longitude > 0) {
-      return 180;
-    } else {
-      return -180 + off;
+    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");
     }
-  }
-
-  /** Keep it simple, we don't need to take arbitrary Random for geo tests */
-  private static Random random() {
-   return RandomizedContext.current().getRandom();
+    
+    // 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("<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"640\" width=\"480\" viewBox=\"");
+    sb.append(minLon - xpadding)
+      .append(" ")
+      .append(90 - maxLat - ypadding)
+      .append(" ")
+      .append(maxLon - minLon + (2*xpadding))
+      .append(" ")
+      .append(maxLat - minLat + (2*ypadding));
+    sb.append("\">\n");
+
+    // encode each object
+    for (Object o : flattened) {
+      // tostring
+      if (o instanceof double[]) {
+        double point[] = (double[]) o;
+        sb.append("<!-- point: ");
+        sb.append(point[0] + "," + point[1]);
+        sb.append(" -->\n");
+      } else {
+        sb.append("<!-- " + o.getClass().getSimpleName() + ": \n");
+        sb.append(o.toString());
+        sb.append("\n-->\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("<polygon fill-opacity=\"" + opacity + "\" points=\"");
+      for (int i = 0; i < polyLats.length; i++) {
+        if (i > 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("<polygon points=\"");
+        for (int i = 0; i < holeLats.length; i++) {
+          if (i > 0) {
+            sb.append(" ");
+          }
+          sb.append(holeLons[i])
+          .append(",")
+          .append(90 - holeLats[i]);
+        }
+        sb.append("\" style=\"fill:lightgray\"/>\n");
+      }
+    }
+    sb.append("</svg>\n");
+    return sb.toString();
   }
 }