blob: f8759514d6a7de511d51f91d972f5b9751926f95 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.geo;
import java.util.Locale;
import java.util.Random;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.lucene.util.SloppyMath;
/**
* Tests class for methods in GeoUtils
*
* @lucene.experimental
*/
public class TestGeoUtils extends LuceneTestCase {
// We rely heavily on GeoUtils.circleToBBox so we test it here:
public void testRandomCircleToBBox() throws Exception {
int iters = atLeast(100);
for(int iter=0;iter<iters;iter++) {
double centerLat = GeoTestUtil.nextLatitude();
double centerLon = GeoTestUtil.nextLongitude();
final double radiusMeters;
if (random().nextBoolean()) {
// Approx 4 degrees lon at the equator:
radiusMeters = random().nextDouble() * 444000;
} else {
radiusMeters = random().nextDouble() * 50000000;
}
// TODO: randomly quantize radius too, to provoke exact math errors?
Rectangle bbox = Rectangle.fromPointDistance(centerLat, centerLon, radiusMeters);
int numPointsToTry = 1000;
for(int i=0;i<numPointsToTry;i++) {
double point[] = GeoTestUtil.nextPointNear(bbox);
double lat = point[0];
double lon = point[1];
double distanceMeters = SloppyMath.haversinMeters(centerLat, centerLon, lat, lon);
// Haversin says it's within the circle:
boolean haversinSays = distanceMeters <= radiusMeters;
// BBox says its within the box:
boolean bboxSays;
if (bbox.crossesDateline()) {
if (lat >= bbox.minLat && lat <= bbox.maxLat) {
bboxSays = lon <= bbox.maxLon || lon >= bbox.minLon;
} else {
bboxSays = false;
}
} else {
bboxSays = lat >= bbox.minLat && lat <= bbox.maxLat && lon >= bbox.minLon && lon <= bbox.maxLon;
}
if (haversinSays) {
if (bboxSays == false) {
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);
fail("point was within the distance according to haversin, but the bbox doesn't contain it");
}
} else {
// it's fine if haversin said it was outside the radius and bbox said it was inside the box
}
}
}
}
// similar to testRandomCircleToBBox, but different, less evil, maybe simpler
public void testBoundingBoxOpto() {
int iters = atLeast(100);
for (int i = 0; i < iters; i++) {
double lat = GeoTestUtil.nextLatitude();
double lon = GeoTestUtil.nextLongitude();
double radius = 50000000 * random().nextDouble();
Rectangle box = Rectangle.fromPointDistance(lat, lon, radius);
final Rectangle box1;
final Rectangle box2;
if (box.crossesDateline()) {
box1 = new Rectangle(box.minLat, box.maxLat, -180, box.maxLon);
box2 = new Rectangle(box.minLat, box.maxLat, box.minLon, 180);
} else {
box1 = box;
box2 = null;
}
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);
assertTrue(lon >= box1.minLon && lon <= box1.maxLon || (box2 != null && lon >= box2.minLon && lon <= box2.maxLon));
}
}
}
}
// test we can use haversinSortKey() for distance queries.
public void testHaversinOpto() {
int iters = atLeast(100);
for (int i = 0; i < iters; i++) {
double lat = GeoTestUtil.nextLatitude();
double lon = GeoTestUtil.nextLongitude();
double radius = 50000000 * random().nextDouble();
Rectangle box = Rectangle.fromPointDistance(lat, lon, radius);
if (box.maxLon - lon < 90 && lon - box.minLon < 90) {
double minPartialDistance = Math.max(SloppyMath.haversinSortKey(lat, lon, lat, box.maxLon),
SloppyMath.haversinSortKey(lat, lon, box.maxLat, lon));
for (int j = 0; j < 10000; j++) {
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);
}
}
}
}
}
/** Test infinite radius covers whole earth */
public void testInfiniteRect() {
for (int i = 0; i < 1000; i++) {
double centerLat = GeoTestUtil.nextLatitude();
double centerLon = GeoTestUtil.nextLongitude();
Rectangle rect = Rectangle.fromPointDistance(centerLat, centerLon, Double.POSITIVE_INFINITY);
assertEquals(-180.0, rect.minLon, 0.0D);
assertEquals(180.0, rect.maxLon, 0.0D);
assertEquals(-90.0, rect.minLat, 0.0D);
assertEquals(90.0, rect.maxLat, 0.0D);
assertFalse(rect.crossesDateline());
}
}
public void testAxisLat() {
double earthCircumference = 2D * Math.PI * GeoUtils.EARTH_MEAN_RADIUS_METERS;
assertEquals(90, Rectangle.axisLat(0, earthCircumference / 4), 0.0D);
for (int i = 0; i < 100; ++i) {
boolean reallyBig = random().nextInt(10) == 0;
final double maxRadius = reallyBig ? 1.1 * earthCircumference : earthCircumference / 8;
final double radius = maxRadius * random().nextDouble();
double prevAxisLat = Rectangle.axisLat(0.0D, radius);
for (double lat = 0.1D; lat < 90D; lat += 0.1D) {
double nextAxisLat = Rectangle.axisLat(lat, radius);
Rectangle bbox = Rectangle.fromPointDistance(lat, 180D, radius);
double dist = SloppyMath.haversinMeters(lat, 180D, nextAxisLat, bbox.maxLon);
if (nextAxisLat < GeoUtils.MAX_LAT_INCL) {
assertEquals("lat = " + lat, dist, radius, 0.1D);
}
assertTrue("lat = " + lat, prevAxisLat <= nextAxisLat);
prevAxisLat = nextAxisLat;
}
prevAxisLat = Rectangle.axisLat(-0.0D, radius);
for (double lat = -0.1D; lat > -90D; lat -= 0.1D) {
double nextAxisLat = Rectangle.axisLat(lat, radius);
Rectangle bbox = Rectangle.fromPointDistance(lat, 180D, radius);
double dist = SloppyMath.haversinMeters(lat, 180D, nextAxisLat, bbox.maxLon);
if (nextAxisLat > GeoUtils.MIN_LAT_INCL) {
assertEquals("lat = " + lat, dist, radius, 0.1D);
}
assertTrue("lat = " + lat, prevAxisLat >= nextAxisLat);
prevAxisLat = nextAxisLat;
}
}
}
// TODO: does not really belong here, but we test it like this for now
// we can make a fake IndexReader to send boxes directly to Point visitors instead?
public void testCircleOpto() throws Exception {
Random random = random();
int iters = atLeast(random, 3);
for (int i = 0; i < iters; i++) {
// circle
final double centerLat = -90 + 180.0 * random.nextDouble();
final double centerLon = -180 + 360.0 * random.nextDouble();
final double radius = 50_000_000D * random.nextDouble();
final Rectangle box = Rectangle.fromPointDistance(centerLat, centerLon, radius);
// TODO: remove this leniency!
if (box.crossesDateline()) {
--i; // try again...
continue;
}
final double axisLat = Rectangle.axisLat(centerLat, radius);
int innerIters = atLeast(100);
for (int k = 0; k < innerIters; ++k) {
double[] latBounds = {-90, box.minLat, axisLat, box.maxLat, 90};
double[] lonBounds = {-180, box.minLon, centerLon, box.maxLon, 180};
// first choose an upper left corner
int maxLatRow = random.nextInt(4);
double latMax = randomInRange(random, latBounds[maxLatRow], latBounds[maxLatRow + 1]);
int minLonCol = random.nextInt(4);
double lonMin = randomInRange(random, lonBounds[minLonCol], lonBounds[minLonCol + 1]);
// now choose a lower right corner
int minLatMaxRow = maxLatRow == 3 ? 3 : maxLatRow + 1; // make sure it will at least cross into the bbox
int minLatRow = random.nextInt(minLatMaxRow);
double latMin = randomInRange(random, latBounds[minLatRow], Math.min(latBounds[minLatRow + 1], latMax));
int maxLonMinCol = Math.max(minLonCol, 1); // make sure it will at least cross into the bbox
int maxLonCol = maxLonMinCol + random.nextInt(4 - maxLonMinCol);
double lonMax = randomInRange(random, Math.max(lonBounds[maxLonCol], lonMin), lonBounds[maxLonCol + 1]);
assert latMax >= latMin;
assert lonMax >= lonMin;
if (isDisjoint(centerLat, centerLon, radius, axisLat, latMin, latMax, lonMin, lonMax)) {
// intersects says false: test a ton of points
for (int j = 0; j < 200; j++) {
double lat = latMin + (latMax - latMin) * random.nextDouble();
double lon = lonMin + (lonMax - lonMin) * random.nextDouble();
if (random.nextBoolean()) {
// explicitly test an edge
int edge = random.nextInt(4);
if (edge == 0) {
lat = latMin;
} else if (edge == 1) {
lat = latMax;
} else if (edge == 2) {
lon = lonMin;
} else if (edge == 3) {
lon = lonMax;
}
}
double distance = SloppyMath.haversinMeters(centerLat, centerLon, lat, lon);
try {
assertTrue(String.format(Locale.ROOT, "\nisDisjoint(\n" +
"centerLat=%s\n" +
"centerLon=%s\n" +
"radius=%s\n" +
"latMin=%s\n" +
"latMax=%s\n" +
"lonMin=%s\n" +
"lonMax=%s) == false BUT\n" +
"haversin(%s, %s, %s, %s) = %s\nbbox=%s",
centerLat, centerLon, radius, latMin, latMax, lonMin, lonMax,
centerLat, centerLon, lat, lon, distance, Rectangle.fromPointDistance(centerLat, centerLon, radius)),
distance > radius);
} catch (AssertionError e) {
EarthDebugger ed = new EarthDebugger();
ed.addRect(latMin, latMax, lonMin, lonMax);
ed.addCircle(centerLat, centerLon, radius, true);
System.out.println(ed.finish());
throw e;
}
}
}
}
}
}
static double randomInRange(Random random, double min, double max) {
return min + (max - min) * random.nextDouble();
}
static boolean isDisjoint(double centerLat, double centerLon, double radius, double axisLat, double latMin, double latMax, double lonMin, double lonMax) {
if ((centerLon < lonMin || centerLon > lonMax) && (axisLat+ Rectangle.AXISLAT_ERROR < latMin || axisLat- Rectangle.AXISLAT_ERROR > latMax)) {
// circle not fully inside / crossing axis
if (SloppyMath.haversinMeters(centerLat, centerLon, latMin, lonMin) > radius &&
SloppyMath.haversinMeters(centerLat, centerLon, latMin, lonMax) > radius &&
SloppyMath.haversinMeters(centerLat, centerLon, latMax, lonMin) > radius &&
SloppyMath.haversinMeters(centerLat, centerLon, latMax, lonMax) > radius) {
// no points inside
return true;
}
}
return false;
}
public void testWithin90LonDegrees() {
assertTrue(GeoUtils.within90LonDegrees(0, -80, 80));
assertFalse(GeoUtils.within90LonDegrees(0, -100, 80));
assertFalse(GeoUtils.within90LonDegrees(0, -80, 100));
assertTrue(GeoUtils.within90LonDegrees(-150, 140, 170));
assertFalse(GeoUtils.within90LonDegrees(-150, 120, 150));
assertTrue(GeoUtils.within90LonDegrees(150, -170, -140));
assertFalse(GeoUtils.within90LonDegrees(150, -150, -120));
}
}