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

This commit is contained in:
Robert Muir 2016-04-18 00:16:56 -04:00
parent aafdc372d9
commit c135e9a7d6
6 changed files with 491 additions and 389 deletions

View File

@ -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 centerLat = GeoTestUtil.nextLatitude();
double centerLon = GeoTestUtil.nextLongitude();
double radiusMeters;
double centerLat = randomLat(useSmallRanges);
double centerLon = randomLon(useSmallRanges);
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);

View File

@ -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));
}
}
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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,113 +30,278 @@ 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 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.
* <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);
}
}
// 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("<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();
}
}