mirror of https://github.com/apache/lucene.git
LUCENE-7147: Improve disjoint check for geo distance query traversal
This commit is contained in:
parent
5958bcb68e
commit
0cf26bf368
|
@ -37,6 +37,9 @@ Optimizations
|
|||
* LUCENE-7115: Speed up FieldCache.CacheEntry toString by setting initial
|
||||
StringBuilder capacity (Gregory Chanan)
|
||||
|
||||
* LUCENE-7147: Improve disjoint check for geo distance query traversal
|
||||
(Ryan Ernst, Robert Muir, Mike McCandless)
|
||||
|
||||
Bug Fixes
|
||||
|
||||
* LUCENE-7127: Fix corner case bugs in GeoPointDistanceQuery. (Robert Muir)
|
||||
|
|
|
@ -108,6 +108,8 @@ final class LatLonPointDistanceQuery extends Query {
|
|||
maxPartialDistance = Double.POSITIVE_INFINITY;
|
||||
}
|
||||
|
||||
final double axisLat = GeoUtils.axisLat(latitude, radiusMeters);
|
||||
|
||||
return new ConstantScoreWeight(this) {
|
||||
|
||||
@Override
|
||||
|
@ -171,8 +173,9 @@ final class LatLonPointDistanceQuery extends Query {
|
|||
|
||||
// algorithm: we create a bounding box (two bounding boxes if we cross the dateline).
|
||||
// 1. check our bounding box(es) first. if the subtree is entirely outside of those, bail.
|
||||
// 2. see if the subtree is fully contained. if the subtree is enormous along the x axis, wrapping half way around the world, etc: then this can't work, just go to step 3.
|
||||
// 3. recurse naively.
|
||||
// 2. check if the subtree is disjoint. it may cross the bounding box but not intersect with circle
|
||||
// 3. see if the subtree is fully contained. if the subtree is enormous along the x axis, wrapping half way around the world, etc: then this can't work, just go to step 3.
|
||||
// 4. recurse naively (subtrees crossing over circle edge)
|
||||
@Override
|
||||
public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
|
||||
if (StringHelper.compare(Integer.BYTES, minPackedValue, 0, maxLat, 0) > 0 ||
|
||||
|
@ -193,6 +196,17 @@ final class LatLonPointDistanceQuery extends Query {
|
|||
double latMax = LatLonPoint.decodeLatitude(maxPackedValue, 0);
|
||||
double lonMax = LatLonPoint.decodeLongitude(maxPackedValue, Integer.BYTES);
|
||||
|
||||
if ((longitude < lonMin || longitude > lonMax) && (axisLat+GeoUtils.AXISLAT_ERROR < latMin || axisLat-GeoUtils.AXISLAT_ERROR > latMax)) {
|
||||
// circle not fully inside / crossing axis
|
||||
if (SloppyMath.haversinMeters(latitude, longitude, latMin, lonMin) > radiusMeters &&
|
||||
SloppyMath.haversinMeters(latitude, longitude, latMin, lonMax) > radiusMeters &&
|
||||
SloppyMath.haversinMeters(latitude, longitude, latMax, lonMin) > radiusMeters &&
|
||||
SloppyMath.haversinMeters(latitude, longitude, latMax, lonMax) > radiusMeters) {
|
||||
// no points inside
|
||||
return Relation.CELL_OUTSIDE_QUERY;
|
||||
}
|
||||
}
|
||||
|
||||
if (lonMax - longitude < 90 && longitude - lonMin < 90 &&
|
||||
SloppyMath.haversinMeters(latitude, longitude, latMin, lonMin) <= radiusMeters &&
|
||||
SloppyMath.haversinMeters(latitude, longitude, latMin, lonMax) <= radiusMeters &&
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.apache.lucene.spatial.geopoint.search;
|
|||
import org.apache.lucene.search.MultiTermQuery;
|
||||
import org.apache.lucene.spatial.geopoint.document.GeoPointField.TermEncoding;
|
||||
import org.apache.lucene.spatial.util.GeoRect;
|
||||
import org.apache.lucene.spatial.util.GeoUtils;
|
||||
import org.apache.lucene.util.SloppyMath;
|
||||
|
||||
/** Package private implementation for the public facing GeoPointDistanceQuery delegate class.
|
||||
|
@ -32,6 +33,9 @@ final class GeoPointDistanceQueryImpl extends GeoPointInBBoxQueryImpl {
|
|||
// optimization, maximum partial haversin needed to be a candidate
|
||||
private final double maxPartialDistance;
|
||||
|
||||
// optimization, used for detecting axis cross
|
||||
final double axisLat;
|
||||
|
||||
GeoPointDistanceQueryImpl(final String field, final TermEncoding termEncoding, final GeoPointDistanceQuery q,
|
||||
final double centerLonUnwrapped, final GeoRect bbox) {
|
||||
super(field, termEncoding, bbox.minLat, bbox.maxLat, bbox.minLon, bbox.maxLon);
|
||||
|
@ -46,6 +50,7 @@ final class GeoPointDistanceQueryImpl extends GeoPointInBBoxQueryImpl {
|
|||
} else {
|
||||
maxPartialDistance = Double.POSITIVE_INFINITY;
|
||||
}
|
||||
axisLat = GeoUtils.axisLat(distanceQuery.centerLat, distanceQuery.radiusMeters);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -65,14 +70,21 @@ final class GeoPointDistanceQueryImpl extends GeoPointInBBoxQueryImpl {
|
|||
|
||||
@Override
|
||||
protected boolean cellCrosses(final double minLat, final double maxLat, final double minLon, final double maxLon) {
|
||||
// bounding box check
|
||||
if (maxLat < GeoPointDistanceQueryImpl.this.minLat ||
|
||||
maxLon < GeoPointDistanceQueryImpl.this.minLon ||
|
||||
minLat > GeoPointDistanceQueryImpl.this.maxLat ||
|
||||
minLon > GeoPointDistanceQueryImpl.this.maxLon) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
} else if ((centerLon < minLon || centerLon > maxLon) && (axisLat+GeoUtils.AXISLAT_ERROR < minLat || axisLat-GeoUtils.AXISLAT_ERROR > maxLat)) {
|
||||
if (SloppyMath.haversinMeters(distanceQuery.centerLat, centerLon, minLat, minLon) > distanceQuery.radiusMeters &&
|
||||
SloppyMath.haversinMeters(distanceQuery.centerLat, centerLon, minLat, maxLon) > distanceQuery.radiusMeters &&
|
||||
SloppyMath.haversinMeters(distanceQuery.centerLat, centerLon, maxLat, minLon) > distanceQuery.radiusMeters &&
|
||||
SloppyMath.haversinMeters(distanceQuery.centerLat, centerLon, maxLat, maxLon) > distanceQuery.radiusMeters) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -109,7 +109,8 @@ public final class GeoUtils {
|
|||
public static GeoRect circleToBBox(final double centerLat, final double centerLon, final double radiusMeters) {
|
||||
final double radLat = TO_RADIANS * centerLat;
|
||||
final double radLon = TO_RADIANS * centerLon;
|
||||
double radDistance = radiusMeters / SEMIMAJOR_AXIS;
|
||||
// LUCENE-7143
|
||||
double radDistance = (radiusMeters + 7E-2) / SEMIMAJOR_AXIS;
|
||||
double minLat = radLat - radDistance;
|
||||
double maxLat = radLat + radDistance;
|
||||
double minLon;
|
||||
|
@ -176,4 +177,51 @@ public final class GeoUtils {
|
|||
return cos(a - PIO2);
|
||||
}
|
||||
|
||||
/** maximum error from {@link #axisLat(double, double)}. logic must be prepared to handle this */
|
||||
public static final double AXISLAT_ERROR = 0.1D / SEMIMAJOR_AXIS * TO_DEGREES;
|
||||
|
||||
/**
|
||||
* Calculate the latitude of a circle's intersections with its bbox meridians.
|
||||
* <p>
|
||||
* <b>NOTE:</b> the returned value will be +/- {@link #AXISLAT_ERROR} of the actual value.
|
||||
* @param centerLat The latitude of the circle center
|
||||
* @param radiusMeters The radius of the circle in meters
|
||||
* @return A latitude
|
||||
*/
|
||||
public static double axisLat(double centerLat, double radiusMeters) {
|
||||
// A spherical triangle with:
|
||||
// r is the radius of the circle in radians
|
||||
// l1 is the latitude of the circle center
|
||||
// l2 is the latitude of the point at which the circle intersect's its bbox longitudes
|
||||
// We know r is tangent to the bbox meridians at l2, therefore it is a right angle.
|
||||
// So from the law of cosines, with the angle of l1 being 90, we have:
|
||||
// cos(l1) = cos(r) * cos(l2) + sin(r) * sin(l2) * cos(90)
|
||||
// The second part cancels out because cos(90) == 0, so we have:
|
||||
// cos(l1) = cos(r) * cos(l2)
|
||||
// Solving for l2, we get:
|
||||
// l2 = acos( cos(l1) / cos(r) )
|
||||
// We ensure r is in the range (0, PI/2) and l1 in the range (0, PI/2]. This means we
|
||||
// cannot divide by 0, and we will always get a positive value in the range [0, 1) as
|
||||
// the argument to arc cosine, resulting in a range (0, PI/2].
|
||||
|
||||
double l1 = TO_RADIANS * centerLat;
|
||||
double r = (radiusMeters + 7E-2) / SEMIMAJOR_AXIS;
|
||||
|
||||
// if we are within radius range of a pole, the lat is the pole itself
|
||||
if (Math.abs(l1) + r >= MAX_LAT_RADIANS) {
|
||||
return centerLat >= 0 ? MAX_LAT_INCL : MIN_LAT_INCL;
|
||||
}
|
||||
|
||||
// adjust l1 as distance from closest pole, to form a right triangle with bbox meridians
|
||||
// and ensure it is in the range (0, PI/2]
|
||||
l1 = centerLat >= 0 ? PIO2 - l1 : l1 + PIO2;
|
||||
|
||||
double l2 = Math.acos(Math.cos(l1) / Math.cos(r));
|
||||
assert !Double.isNaN(l2);
|
||||
|
||||
// now adjust back to range [-pi/2, pi/2], ie latitude in radians
|
||||
l2 = centerLat >= 0 ? PIO2 - l2 : l2 - PIO2;
|
||||
|
||||
return TO_DEGREES * l2;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,8 +17,11 @@
|
|||
package org.apache.lucene.spatial.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import org.apache.lucene.util.SloppyMath;
|
||||
|
||||
import com.carrotsearch.randomizedtesting.RandomizedContext;
|
||||
|
||||
/** static methods for testing geo */
|
||||
|
@ -266,4 +269,232 @@ public class GeoTestUtil {
|
|||
private static Random random() {
|
||||
return RandomizedContext.current().getRandom();
|
||||
}
|
||||
|
||||
// craziness for plotting stuff :)
|
||||
|
||||
private static double wrapLat(double lat) {
|
||||
//System.out.println("wrapLat " + lat);
|
||||
if (lat > 90) {
|
||||
//System.out.println(" " + (180 - lat));
|
||||
return 180 - lat;
|
||||
} else if (lat < -90) {
|
||||
//System.out.println(" " + (-180 - lat));
|
||||
return -180 - lat;
|
||||
} else {
|
||||
//System.out.println(" " + lat);
|
||||
return lat;
|
||||
}
|
||||
}
|
||||
|
||||
private static double wrapLon(double lon) {
|
||||
//System.out.println("wrapLon " + lon);
|
||||
if (lon > 180) {
|
||||
//System.out.println(" " + (lon - 360));
|
||||
return lon - 360;
|
||||
} else if (lon < -180) {
|
||||
//System.out.println(" " + (lon + 360));
|
||||
return lon + 360;
|
||||
} else {
|
||||
//System.out.println(" " + lon);
|
||||
return lon;
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawRectApproximatelyOnEarthSurface(String name, String color, double minLat, double maxLat, double minLon, double maxLon) {
|
||||
int steps = 20;
|
||||
System.out.println(" var " + name + " = WE.polygon([");
|
||||
System.out.println(" // min -> max lat, min lon");
|
||||
for(int i=0;i<steps;i++) {
|
||||
System.out.println(" [" + (minLat + (maxLat - minLat) * i / steps) + ", " + minLon + "],");
|
||||
}
|
||||
System.out.println(" // max lat, min -> max lon");
|
||||
for(int i=0;i<steps;i++) {
|
||||
System.out.println(" [" + (maxLat + ", " + (minLon + (maxLon - minLon) * i / steps)) + "],");
|
||||
}
|
||||
System.out.println(" // max -> min lat, max lon");
|
||||
for(int i=0;i<steps;i++) {
|
||||
System.out.println(" [" + (minLat + (maxLat - minLat) * (steps-i) / steps) + ", " + maxLon + "],");
|
||||
}
|
||||
System.out.println(" // min lat, max -> min lon");
|
||||
for(int i=0;i<steps;i++) {
|
||||
System.out.println(" [" + minLat + ", " + (minLon + (maxLon - minLon) * (steps-i) / steps) + "],");
|
||||
}
|
||||
System.out.println(" // min lat, min lon");
|
||||
System.out.println(" [" + minLat + ", " + minLon + "]");
|
||||
System.out.println(" ], {color: \"" + color + "\", fillColor: \"" + color + "\"});");
|
||||
System.out.println(" " + name + ".addTo(earth);");
|
||||
}
|
||||
|
||||
private static void plotLatApproximatelyOnEarthSurface(String name, String color, double lat, double minLon, double maxLon) {
|
||||
System.out.println(" var " + name + " = WE.polygon([");
|
||||
double lon;
|
||||
for(lon = minLon;lon<=maxLon;lon += (maxLon-minLon)/36) {
|
||||
System.out.println(" [" + lat + ", " + lon + "],");
|
||||
}
|
||||
System.out.println(" [" + lat + ", " + maxLon + "],");
|
||||
lon -= (maxLon-minLon)/36;
|
||||
for(;lon>=minLon;lon -= (maxLon-minLon)/36) {
|
||||
System.out.println(" [" + lat + ", " + lon + "],");
|
||||
}
|
||||
System.out.println(" ], {color: \"" + color + "\", fillColor: \"#ffffff\", opacity: " + (color.equals("#ffffff") ? "0.3" : "1") + ", fillOpacity: 0.0001});");
|
||||
System.out.println(" " + name + ".addTo(earth);");
|
||||
}
|
||||
|
||||
private static void plotLonApproximatelyOnEarthSurface(String name, String color, double lon, double minLat, double maxLat) {
|
||||
System.out.println(" var " + name + " = WE.polygon([");
|
||||
double lat;
|
||||
for(lat = minLat;lat<=maxLat;lat += (maxLat-minLat)/36) {
|
||||
System.out.println(" [" + lat + ", " + lon + "],");
|
||||
}
|
||||
System.out.println(" [" + maxLat + ", " + lon + "],");
|
||||
lat -= (maxLat-minLat)/36;
|
||||
for(;lat>=minLat;lat -= (maxLat-minLat)/36) {
|
||||
System.out.println(" [" + lat + ", " + lon + "],");
|
||||
}
|
||||
System.out.println(" ], {color: \"" + color + "\", fillColor: \"#ffffff\", opacity: " + (color.equals("#ffffff") ? "0.3" : "1") + ", fillOpacity: 0.0001});");
|
||||
System.out.println(" " + name + ".addTo(earth);");
|
||||
}
|
||||
|
||||
// http://www.webglearth.org has API details:
|
||||
public static void polysToWebGLEarth(List<double[][]> polys) {
|
||||
System.out.println("<!DOCTYPE HTML>");
|
||||
System.out.println("<html>");
|
||||
System.out.println(" <head>");
|
||||
System.out.println(" <script src=\"http://www.webglearth.com/v2/api.js\"></script>");
|
||||
System.out.println(" <script>");
|
||||
System.out.println(" function initialize() {");
|
||||
System.out.println(" var earth = new WE.map('earth_div');");
|
||||
|
||||
int count = 0;
|
||||
for (double[][] poly : polys) {
|
||||
System.out.println(" var poly" + count + " = WE.polygon([");
|
||||
for(int i=0;i<poly[0].length;i++) {
|
||||
double lat = poly[0][i];
|
||||
double lon = poly[1][i];
|
||||
System.out.println(" [" + lat + ", " + lon + "],");
|
||||
}
|
||||
System.out.println(" ], {color: '#00ff00'});");
|
||||
System.out.println(" poly" + count + ".addTo(earth);");
|
||||
}
|
||||
|
||||
System.out.println(" WE.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{");
|
||||
System.out.println(" attribution: '© OpenStreetMap contributors'");
|
||||
System.out.println(" }).addTo(earth);");
|
||||
System.out.println(" }");
|
||||
System.out.println(" </script>");
|
||||
System.out.println(" <style>");
|
||||
System.out.println(" html, body{padding: 0; margin: 0;}");
|
||||
System.out.println(" #earth_div{top: 0; right: 0; bottom: 0; left: 0; position: absolute !important;}");
|
||||
System.out.println(" </style>");
|
||||
System.out.println(" <title>WebGL Earth API: Hello World</title>");
|
||||
System.out.println(" </head>");
|
||||
System.out.println(" <body onload=\"initialize()\">");
|
||||
System.out.println(" <div id=\"earth_div\"></div>");
|
||||
System.out.println(" </body>");
|
||||
System.out.println("</html>");
|
||||
}
|
||||
|
||||
// http://www.webglearth.org has API details:
|
||||
public static void toWebGLEarth(double rectMinLatitude, double rectMaxLatitude,
|
||||
double rectMinLongitude, double rectMaxLongitude,
|
||||
double centerLatitude, double centerLongitude,
|
||||
double radiusMeters) {
|
||||
GeoRect box = GeoUtils.circleToBBox(centerLatitude, centerLongitude, radiusMeters);
|
||||
System.out.println("<!DOCTYPE HTML>");
|
||||
System.out.println("<html>");
|
||||
System.out.println(" <head>");
|
||||
System.out.println(" <script src=\"http://www.webglearth.com/v2/api.js\"></script>");
|
||||
System.out.println(" <script>");
|
||||
System.out.println(" function initialize() {");
|
||||
System.out.println(" var earth = new WE.map('earth_div', {center: [" + centerLatitude + ", " + centerLongitude + "]});");
|
||||
System.out.println(" var marker = WE.marker([" + centerLatitude + ", " + centerLongitude + "]).addTo(earth);");
|
||||
drawRectApproximatelyOnEarthSurface("cell", "#ff0000", rectMinLatitude, rectMaxLatitude, rectMinLongitude, rectMaxLongitude);
|
||||
System.out.println(" var polygonB = WE.polygon([");
|
||||
StringBuilder b = new StringBuilder();
|
||||
inverseHaversin(b, centerLatitude, centerLongitude, radiusMeters);
|
||||
System.out.println(b);
|
||||
System.out.println(" ], {color: '#00ff00'});");
|
||||
System.out.println(" polygonB.addTo(earth);");
|
||||
drawRectApproximatelyOnEarthSurface("bbox", "#00ff00", box.minLat, box.maxLat, box.minLon, box.maxLon);
|
||||
System.out.println(" WE.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{");
|
||||
System.out.println(" attribution: '© OpenStreetMap contributors'");
|
||||
System.out.println(" }).addTo(earth);");
|
||||
plotLatApproximatelyOnEarthSurface("lat0", "#ffffff", 4.68, 0.0, 360.0);
|
||||
plotLatApproximatelyOnEarthSurface("lat1", "#ffffff", 180-93.09, 0.0, 360.0);
|
||||
plotLatApproximatelyOnEarthSurface("axisLat", "#00ff00", GeoUtils.axisLat(centerLatitude, radiusMeters), box.minLon, box.maxLon);
|
||||
plotLonApproximatelyOnEarthSurface("axisLon", "#00ff00", centerLongitude, box.minLat, box.maxLat);
|
||||
System.out.println(" }");
|
||||
System.out.println(" </script>");
|
||||
System.out.println(" <style>");
|
||||
System.out.println(" html, body{padding: 0; margin: 0;}");
|
||||
System.out.println(" #earth_div{top: 0; right: 0; bottom: 0; left: 0; position: absolute !important;}");
|
||||
System.out.println(" </style>");
|
||||
System.out.println(" <title>WebGL Earth API: Hello World</title>");
|
||||
System.out.println(" </head>");
|
||||
System.out.println(" <body onload=\"initialize()\">");
|
||||
System.out.println(" <div id=\"earth_div\"></div>");
|
||||
System.out.println(" </body>");
|
||||
System.out.println("</html>");
|
||||
}
|
||||
|
||||
private static void inverseHaversin(StringBuilder b, double centerLat, double centerLon, double radiusMeters) {
|
||||
double angle = 0;
|
||||
int steps = 100;
|
||||
|
||||
newAngle:
|
||||
while (angle < 360) {
|
||||
double x = Math.cos(Math.toRadians(angle));
|
||||
double y = Math.sin(Math.toRadians(angle));
|
||||
double factor = 2.0;
|
||||
double step = 1.0;
|
||||
int last = 0;
|
||||
double lastDistanceMeters = 0.0;
|
||||
//System.out.println("angle " + angle + " slope=" + slope);
|
||||
while (true) {
|
||||
double lat = wrapLat(centerLat + y * factor);
|
||||
double lon = wrapLon(centerLon + x * factor);
|
||||
double distanceMeters = SloppyMath.haversinMeters(centerLat, centerLon, lat, lon);
|
||||
|
||||
if (last == 1 && distanceMeters < lastDistanceMeters) {
|
||||
// For large enough circles, some angles are not possible:
|
||||
//System.out.println(" done: give up on angle " + angle);
|
||||
angle += 360./steps;
|
||||
continue newAngle;
|
||||
}
|
||||
if (last == -1 && distanceMeters > lastDistanceMeters) {
|
||||
// For large enough circles, some angles are not possible:
|
||||
//System.out.println(" done: give up on angle " + angle);
|
||||
angle += 360./steps;
|
||||
continue newAngle;
|
||||
}
|
||||
lastDistanceMeters = distanceMeters;
|
||||
|
||||
//System.out.println(" iter lat=" + lat + " lon=" + lon + " distance=" + distanceMeters + " vs " + radiusMeters);
|
||||
if (Math.abs(distanceMeters - radiusMeters) < 0.1) {
|
||||
b.append(" [" + lat + ", " + lon + "],\n");
|
||||
break;
|
||||
}
|
||||
if (distanceMeters > radiusMeters) {
|
||||
// too big
|
||||
//System.out.println(" smaller");
|
||||
factor -= step;
|
||||
if (last == 1) {
|
||||
//System.out.println(" half-step");
|
||||
step /= 2.0;
|
||||
}
|
||||
last = -1;
|
||||
} else if (distanceMeters < radiusMeters) {
|
||||
// too small
|
||||
//System.out.println(" bigger");
|
||||
factor += step;
|
||||
if (last == -1) {
|
||||
//System.out.println(" half-step");
|
||||
step /= 2.0;
|
||||
}
|
||||
last = 1;
|
||||
}
|
||||
}
|
||||
angle += 360./steps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
*/
|
||||
package org.apache.lucene.spatial.util;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.apache.lucene.util.BytesRefBuilder;
|
||||
import org.apache.lucene.util.LuceneTestCase;
|
||||
import org.apache.lucene.util.SloppyMath;
|
||||
|
@ -320,4 +322,136 @@ public class TestGeoUtils extends LuceneTestCase {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void testAxisLat() {
|
||||
double earthCircumference = 2D * Math.PI * GeoUtils.SEMIMAJOR_AXIS;
|
||||
assertEquals(90, GeoUtils.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 = GeoUtils.axisLat(0.0D, radius);
|
||||
for (double lat = 0.1D; lat < 90D; lat += 0.1D) {
|
||||
double nextAxisLat = GeoUtils.axisLat(lat, radius);
|
||||
GeoRect bbox = GeoUtils.circleToBBox(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 = GeoUtils.axisLat(-0.0D, radius);
|
||||
for (double lat = -0.1D; lat > -90D; lat -= 0.1D) {
|
||||
double nextAxisLat = GeoUtils.axisLat(lat, radius);
|
||||
GeoRect bbox = GeoUtils.circleToBBox(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 {
|
||||
for (int i = 0; i < 50; 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 GeoRect box = GeoUtils.circleToBBox(centerLat, centerLon, radius);
|
||||
// TODO: remove this leniency!
|
||||
if (box.crossesDateline()) {
|
||||
--i; // try again...
|
||||
continue;
|
||||
}
|
||||
final double axisLat = GeoUtils.axisLat(centerLat, radius);
|
||||
|
||||
for (int k = 0; k < 1000; ++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(latBounds[maxLatRow], latBounds[maxLatRow + 1]);
|
||||
int minLonCol = random().nextInt(4);
|
||||
double lonMin = randomInRange(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(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(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, GeoUtils.circleToBBox(centerLat, centerLon, radius)),
|
||||
distance > radius);
|
||||
} catch (AssertionError e) {
|
||||
GeoTestUtil.toWebGLEarth(latMin, latMax, lonMin, lonMax, centerLat, centerLon, radius);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static double randomInRange(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+GeoUtils.AXISLAT_ERROR < latMin || axisLat-GeoUtils.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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue