LUCENE-6908: Fix TestGeoUtils.testGeoRelations to handle irregular rectangles. Refactors relational methods to new GeoRelationUtils class.

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1718481 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Nick Knize 2015-12-07 22:02:48 +00:00
parent 6d8a195822
commit 92b659ab9e
14 changed files with 602 additions and 340 deletions

View File

@ -116,6 +116,10 @@ New Features
API Changes
* LUCENE-6908: GeoUtils static relational methods have been refactored to new
GeoRelationUtils and now correctly handle large irregular rectangles, and
pole crossing distance queries. (Nick Knize)
* LUCENE-6900: Grouping sortWithinGroup variables used to allow null to mean
Sort.RELEVANCE. Null is no longer permitted. (David Smiley)

View File

@ -87,7 +87,41 @@ public class SloppyMath {
double indexSin = sinTab[index];
return indexCos + delta * (-indexSin + delta * (-indexCos * ONE_DIV_F2 + delta * (indexSin * ONE_DIV_F3 + delta * indexCos * ONE_DIV_F4)));
}
/**
* Returns the trigonometric sine of an angle converted as a cos operation.
* <p>
* Error is around 1E-15.
* <p>
* Special cases:
* <ul>
* <li>If the argument is {@code NaN} or an infinity, then the result is {@code NaN}.
* </ul>
* @param a an angle, in radians.
* @return the sine of the argument.
* @see Math#cos(double)
*/
public static double sin(double a) {
return cos(a - PIO2);
}
/**
* Returns the trigonometric tangent of an angle converted in terms of a sin/cos operation.
* <p>
* Error is around 1E-15.
* <p>
* Special cases:
* <ul>
* <li>If the argument is {@code NaN} or an infinity, then the result is {@code NaN}.
* </ul>
* @param a an angle, in radians.
* @return the tangent of the argument.
* @see Math#sin(double) aand Math#cos(double)
*/
public static double tan(double a) {
return sin(a) / cos(a);
}
/**
* Returns the arc sine of a value.
* <p>
@ -146,13 +180,15 @@ public class SloppyMath {
}
// haversin
private static final double TO_RADIANS = Math.PI / 180D;
static final double TO_RADIANS = Math.PI / 180D;
static final double TO_DEGREES = 180D / Math.PI;
// cos/asin
private static final double ONE_DIV_F2 = 1/2.0;
private static final double ONE_DIV_F3 = 1/6.0;
private static final double ONE_DIV_F4 = 1/24.0;
static final double PIO2 = Math.PI / 2D;
private static final double PIO2_HI = Double.longBitsToDouble(0x3FF921FB54400000L); // 1.57079632673412561417e+00 first 33 bits of pi/2
private static final double PIO2_LO = Double.longBitsToDouble(0x3DD0B4611A626331L); // 6.07710050650619224932e-11 pi/2 - PIO2_HI
private static final double TWOPI_HI = 4*PIO2_HI;

View File

@ -27,6 +27,7 @@ import org.apache.lucene.index.DimensionalValues.Relation;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.util.DocIdSetBuilder;
import org.apache.lucene.util.GeoRelationUtils;
import org.apache.lucene.util.GeoUtils;
import org.apache.lucene.util.bkd.BKDUtil;
@ -125,7 +126,7 @@ public class DimensionalPointInPolygonQuery extends Query {
assert packedValue.length == 8;
double lat = DimensionalLatLonField.decodeLat(BKDUtil.bytesToInt(packedValue, 0));
double lon = DimensionalLatLonField.decodeLon(BKDUtil.bytesToInt(packedValue, 1));
if (GeoUtils.pointInPolygon(polyLons, polyLats, lat, lon)) {
if (GeoRelationUtils.pointInPolygon(polyLons, polyLats, lat, lon)) {
hitCount[0]++;
result.add(docID);
}
@ -141,11 +142,11 @@ public class DimensionalPointInPolygonQuery extends Query {
if (cellMinLat <= minLat && cellMaxLat >= maxLat && cellMinLon <= minLon && cellMaxLon >= maxLon) {
// Cell fully encloses the query
return Relation.CELL_CROSSES_QUERY;
} else if (GeoUtils.rectWithinPoly(cellMinLon, cellMinLat, cellMaxLon, cellMaxLat,
} else if (GeoRelationUtils.rectWithinPoly(cellMinLon, cellMinLat, cellMaxLon, cellMaxLat,
polyLons, polyLats,
minLon, minLat, maxLon, maxLat)) {
return Relation.CELL_INSIDE_QUERY;
} else if (GeoUtils.rectCrossesPoly(cellMinLon, cellMinLat, cellMaxLon, cellMaxLat,
} else if (GeoRelationUtils.rectCrossesPoly(cellMinLon, cellMinLat, cellMaxLon, cellMaxLat,
polyLons, polyLats,
minLon, minLat, maxLon, maxLat)) {
return Relation.CELL_CROSSES_QUERY;

View File

@ -24,6 +24,7 @@ import org.apache.lucene.index.Terms;
import org.apache.lucene.index.TermsEnum;
import org.apache.lucene.util.AttributeSource;
import org.apache.lucene.util.GeoRect;
import org.apache.lucene.util.GeoRelationUtils;
import org.apache.lucene.util.GeoUtils;
import org.apache.lucene.util.SloppyMath;
@ -77,12 +78,14 @@ final class GeoPointDistanceQueryImpl extends GeoPointInBBoxQueryImpl {
@Override
protected boolean cellCrosses(final double minLon, final double minLat, final double maxLon, final double maxLat) {
return GeoUtils.rectCrossesCircle(minLon, minLat, maxLon, maxLat, centerLon, query.centerLat, query.radiusMeters);
return GeoRelationUtils.rectCrossesCircle(minLon, minLat, maxLon, maxLat,
centerLon, query.centerLat, query.radiusMeters, true);
}
@Override
protected boolean cellWithin(final double minLon, final double minLat, final double maxLon, final double maxLat) {
return GeoUtils.rectWithinCircle(minLon, minLat, maxLon, maxLat, centerLon, query.centerLat, query.radiusMeters);
return GeoRelationUtils.rectWithinCircle(minLon, minLat, maxLon, maxLat,
centerLon, query.centerLat, query.radiusMeters, true);
}
@Override

View File

@ -23,6 +23,7 @@ import org.apache.lucene.document.GeoPointField;
import org.apache.lucene.index.Terms;
import org.apache.lucene.index.TermsEnum;
import org.apache.lucene.util.AttributeSource;
import org.apache.lucene.util.GeoRelationUtils;
import org.apache.lucene.util.GeoUtils;
import org.apache.lucene.util.SloppyMath;
import org.apache.lucene.util.ToStringUtils;
@ -83,7 +84,7 @@ class GeoPointInBBoxQueryImpl extends GeoPointTermQuery {
*/
@Override
protected boolean cellCrosses(final double minLon, final double minLat, final double maxLon, final double maxLat) {
return GeoUtils.rectCrosses(minLon, minLat, maxLon, maxLat, this.minLon, this.minLat, this.maxLon, this.maxLat);
return GeoRelationUtils.rectCrosses(minLon, minLat, maxLon, maxLat, this.minLon, this.minLat, this.maxLon, this.maxLat);
}
/**
@ -91,7 +92,7 @@ class GeoPointInBBoxQueryImpl extends GeoPointTermQuery {
*/
@Override
protected boolean cellWithin(final double minLon, final double minLat, final double maxLon, final double maxLat) {
return GeoUtils.rectWithin(minLon, minLat, maxLon, maxLat, this.minLon, this.minLat, this.maxLon, this.maxLat);
return GeoRelationUtils.rectWithin(minLon, minLat, maxLon, maxLat, this.minLon, this.minLat, this.maxLon, this.maxLat);
}
@Override
@ -101,7 +102,7 @@ class GeoPointInBBoxQueryImpl extends GeoPointTermQuery {
@Override
protected boolean postFilter(final double lon, final double lat) {
return GeoUtils.bboxContains(lon, lat, minLon, minLat, maxLon, maxLat);
return GeoRelationUtils.pointInRect(lon, lat, minLon, minLat, maxLon, maxLat);
}
}

View File

@ -24,6 +24,7 @@ import org.apache.lucene.index.Terms;
import org.apache.lucene.index.TermsEnum;
import org.apache.lucene.util.AttributeSource;
import org.apache.lucene.util.GeoRect;
import org.apache.lucene.util.GeoRelationUtils;
import org.apache.lucene.util.GeoUtils;
/** Implements a simple point in polygon query on a GeoPoint field. This is based on
@ -147,13 +148,13 @@ public final class GeoPointInPolygonQuery extends GeoPointInBBoxQueryImpl {
@Override
protected boolean cellCrosses(final double minLon, final double minLat, final double maxLon, final double maxLat) {
return GeoUtils.rectCrossesPoly(minLon, minLat, maxLon, maxLat, x, y, GeoPointInPolygonQuery.this.minLon,
return GeoRelationUtils.rectCrossesPoly(minLon, minLat, maxLon, maxLat, x, y, GeoPointInPolygonQuery.this.minLon,
GeoPointInPolygonQuery.this.minLat, GeoPointInPolygonQuery.this.maxLon, GeoPointInPolygonQuery.this.maxLat);
}
@Override
protected boolean cellWithin(final double minLon, final double minLat, final double maxLon, final double maxLat) {
return GeoUtils.rectWithinPoly(minLon, minLat, maxLon, maxLat, x, y, GeoPointInPolygonQuery.this.minLon,
return GeoRelationUtils.rectWithinPoly(minLon, minLat, maxLon, maxLat, x, y, GeoPointInPolygonQuery.this.minLon,
GeoPointInPolygonQuery.this.minLat, GeoPointInPolygonQuery.this.maxLon, GeoPointInPolygonQuery.this.maxLat);
}
@ -168,11 +169,11 @@ public final class GeoPointInPolygonQuery extends GeoPointInBBoxQueryImpl {
* {@link org.apache.lucene.search.GeoPointTermsEnum#accept} method is called to match
* encoded terms that fall within the bounding box of the polygon. Those documents that pass the initial
* bounding box filter are then compared to the provided polygon using the
* {@link org.apache.lucene.util.GeoUtils#pointInPolygon} method.
* {@link org.apache.lucene.util.GeoRelationUtils#pointInPolygon} method.
*/
@Override
protected boolean postFilter(final double lon, final double lat) {
return GeoUtils.pointInPolygon(x, y, lat, lon);
return GeoRelationUtils.pointInPolygon(x, y, lat, lon);
}
}

View File

@ -26,6 +26,7 @@ import org.apache.lucene.index.FilteredTermsEnum;
import org.apache.lucene.index.TermsEnum;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.BytesRefBuilder;
import org.apache.lucene.util.GeoRelationUtils;
import org.apache.lucene.util.GeoUtils;
import org.apache.lucene.util.NumericUtils;
@ -139,14 +140,14 @@ abstract class GeoPointTermsEnum extends FilteredTermsEnum {
* Primary driver for cells intersecting shape boundaries
*/
protected boolean cellIntersectsMBR(final double minLon, final double minLat, final double maxLon, final double maxLat) {
return GeoUtils.rectIntersects(minLon, minLat, maxLon, maxLat, this.minLon, this.minLat, this.maxLon, this.maxLat);
return GeoRelationUtils.rectIntersects(minLon, minLat, maxLon, maxLat, this.minLon, this.minLat, this.maxLon, this.maxLat);
}
/**
* Return whether quad-cell contains the bounding box of this shape
*/
protected boolean cellContains(final double minLon, final double minLat, final double maxLon, final double maxLat) {
return GeoUtils.rectWithin(this.minLon, this.minLat, this.maxLon, this.maxLat, minLon, minLat, maxLon, maxLat);
return GeoRelationUtils.rectWithin(this.minLon, this.minLat, this.maxLon, this.maxLat, minLon, minLat, maxLon, maxLat);
}
public boolean boundaryTerm() {

View File

@ -17,6 +17,8 @@ package org.apache.lucene.util;
* limitations under the License.
*/
import static org.apache.lucene.util.SloppyMath.TO_RADIANS;
/**
* Reusable geo-spatial distance utility methods.
*
@ -24,6 +26,28 @@ package org.apache.lucene.util;
*/
public class GeoDistanceUtils {
/**
* Compute the great-circle distance using original haversine implementation published by Sinnot in:
* R.W. Sinnott, "Virtues of the Haversine", Sky and Telescope, vol. 68, no. 2, 1984, p. 159
*
* NOTE: this differs from {@link org.apache.lucene.util.SloppyMath#haversin} in that it uses the semi-major axis
* of the earth instead of an approximation based on the average latitude of the two points (which can introduce an
* additional error up to .337%, or ~67.6 km at the equator)
*/
public static double haversin(double lat1, double lon1, double lat2, double lon2) {
double dLat = TO_RADIANS * (lat2 - lat1);
double dLon = TO_RADIANS * (lon2 - lon1);
lat1 = TO_RADIANS * (lat1);
lat2 = TO_RADIANS * (lat2);
final double sinDLatO2 = SloppyMath.sin(dLat / 2);
final double sinDLonO2 = SloppyMath.sin(dLon / 2);
double a = sinDLatO2*sinDLatO2 + sinDLonO2 * sinDLonO2 * SloppyMath.cos(lat1) * SloppyMath.cos(lat2);
double c = 2 * SloppyMath.asin(Math.sqrt(a));
return (GeoProjectionUtils.SEMIMAJOR_AXIS * c);
}
/**
* Compute the distance between two geo-points using vincenty distance formula
* Vincenty uses the oblate spheroid whereas haversine uses unit sphere, this will give roughly
@ -86,6 +110,20 @@ public class GeoDistanceUtils {
return (GeoProjectionUtils.SEMIMINOR_AXIS * A * (sigma - deltaSigma));
}
/**
* Computes distance between two points in a cartesian (x, y, {z - optional}) coordinate system
*/
public static double linearDistance(double[] pt1, double[] pt2) {
assert pt1 != null && pt2 != null && pt1.length == pt2.length && pt1.length > 1;
final double d0 = pt1[0] - pt2[0];
final double d1 = pt1[1] - pt2[1];
if (pt1.length == 3) {
final double d2 = pt1[2] - pt2[2];
return Math.sqrt(d0*d0 + d1*d1 + d2*d2);
}
return Math.sqrt(d0*d0 + d1*d1);
}
/**
* Compute the inverse haversine to determine distance in degrees longitude for provided distance in meters
* @param lat latitude to compute delta degrees lon
@ -152,9 +190,9 @@ public class GeoDistanceUtils {
/** Returns the maximum distance/radius (in meters) from the point 'center' before overlapping */
public static double maxRadialDistanceMeters(final double centerLon, final double centerLat) {
if (Math.abs(centerLat) == GeoUtils.MAX_LAT_INCL) {
return SloppyMath.haversin(centerLat, centerLon, 0, centerLon)*1000.0;
return GeoDistanceUtils.haversin(centerLat, centerLon, 0, centerLon);
}
return SloppyMath.haversin(centerLat, centerLon, centerLat, (GeoUtils.MAX_LON_INCL + centerLon) % 360)*1000.0;
return GeoDistanceUtils.haversin(centerLat, centerLon, centerLat, (GeoUtils.MAX_LON_INCL + centerLon) % 360);
}
/**

View File

@ -17,6 +17,10 @@ package org.apache.lucene.util;
* limitations under the License.
*/
import static org.apache.lucene.util.SloppyMath.PIO2;
import static org.apache.lucene.util.SloppyMath.TO_DEGREES;
import static org.apache.lucene.util.SloppyMath.TO_RADIANS;
/**
* Reusable geo-spatial projection utility methods.
*
@ -28,13 +32,14 @@ public class GeoProjectionUtils {
public static final double FLATTENING = 1.0/298.257223563;
public static final double SEMIMINOR_AXIS = SEMIMAJOR_AXIS * (1.0 - FLATTENING); //6_356_752.31420; // [m]
public static final double ECCENTRICITY = StrictMath.sqrt((2.0 - FLATTENING) * FLATTENING);
static final double PI_OVER_2 = StrictMath.PI / 2.0D;
static final double SEMIMAJOR_AXIS2 = SEMIMAJOR_AXIS * SEMIMAJOR_AXIS;
static final double SEMIMINOR_AXIS2 = SEMIMINOR_AXIS * SEMIMINOR_AXIS;
public static final double MIN_LON_RADIANS = StrictMath.toRadians(GeoUtils.MIN_LON_INCL);
public static final double MIN_LAT_RADIANS = StrictMath.toRadians(GeoUtils.MIN_LAT_INCL);
public static final double MAX_LON_RADIANS = StrictMath.toRadians(GeoUtils.MAX_LON_INCL);
public static final double MAX_LAT_RADIANS = StrictMath.toRadians(GeoUtils.MAX_LAT_INCL);
public static final double MIN_LON_RADIANS = TO_RADIANS * GeoUtils.MIN_LON_INCL;
public static final double MIN_LAT_RADIANS = TO_RADIANS * GeoUtils.MIN_LAT_INCL;
public static final double MAX_LON_RADIANS = TO_RADIANS * GeoUtils.MAX_LON_INCL;
public static final double MAX_LAT_RADIANS = TO_RADIANS * GeoUtils.MAX_LAT_INCL;
private static final double E2 = (SEMIMAJOR_AXIS2 - SEMIMINOR_AXIS2)/(SEMIMAJOR_AXIS2);
private static final double EP2 = (SEMIMAJOR_AXIS2 - SEMIMINOR_AXIS2)/(SEMIMINOR_AXIS2);
/**
* Converts from geocentric earth-centered earth-fixed to geodesic lat/lon/alt
@ -47,8 +52,6 @@ public class GeoProjectionUtils {
public static final double[] ecfToLLA(final double x, final double y, final double z, double[] lla) {
boolean atPole = false;
final double ad_c = 1.0026000D;
final double e2 = (SEMIMAJOR_AXIS2 - SEMIMINOR_AXIS2)/(SEMIMAJOR_AXIS2);
final double ep2 = (SEMIMAJOR_AXIS2 - SEMIMINOR_AXIS2)/(SEMIMINOR_AXIS2);
final double cos67P5 = 0.38268343236508977D;
if (lla == null) {
@ -59,18 +62,18 @@ public class GeoProjectionUtils {
lla[0] = StrictMath.atan2(y,x);
} else {
if (y > 0) {
lla[0] = PI_OVER_2;
lla[0] = PIO2;
} else if (y < 0) {
lla[0] = -PI_OVER_2;
lla[0] = -PIO2;
} else {
atPole = true;
lla[0] = 0.0D;
if (z > 0.0) {
lla[1] = PI_OVER_2;
lla[1] = PIO2;
} else if (z < 0.0) {
lla[1] = -PI_OVER_2;
lla[1] = -PIO2;
} else {
lla[1] = PI_OVER_2;
lla[1] = PIO2;
lla[2] = -SEMIMINOR_AXIS;
return lla;
}
@ -84,25 +87,25 @@ public class GeoProjectionUtils {
final double sinB0 = t0 / s0;
final double cosB0 = w / s0;
final double sin3B0 = sinB0 * sinB0 * sinB0;
final double t1 = z + SEMIMINOR_AXIS * ep2 * sin3B0;
final double sum = w - SEMIMAJOR_AXIS * e2 * cosB0 * cosB0 * cosB0;
final double t1 = z + SEMIMINOR_AXIS * EP2 * sin3B0;
final double sum = w - SEMIMAJOR_AXIS * E2 * cosB0 * cosB0 * cosB0;
final double s1 = StrictMath.sqrt(t1 * t1 + sum * sum);
final double sinP1 = t1 / s1;
final double cosP1 = sum / s1;
final double rn = SEMIMAJOR_AXIS / StrictMath.sqrt(1.0D - e2 * sinP1 * sinP1);
final double rn = SEMIMAJOR_AXIS / StrictMath.sqrt(1.0D - E2 * sinP1 * sinP1);
if (cosP1 >= cos67P5) {
lla[2] = w / cosP1 - rn;
} else if (cosP1 <= -cos67P5) {
lla[2] = w / -cosP1 - rn;
} else {
lla[2] = z / sinP1 + rn * (e2 - 1.0);
lla[2] = z / sinP1 + rn * (E2 - 1.0);
}
if (!atPole) {
lla[1] = StrictMath.atan(sinP1/cosP1);
}
lla[0] = StrictMath.toDegrees(lla[0]);
lla[1] = StrictMath.toDegrees(lla[1]);
lla[0] = TO_DEGREES * lla[0];
lla[1] = TO_DEGREES * lla[1];
return lla;
}
@ -116,33 +119,32 @@ public class GeoProjectionUtils {
* @return either a new ecef array or the reusable ecf parameter
*/
public static final double[] llaToECF(double lon, double lat, double alt, double[] ecf) {
lon = StrictMath.toRadians(lon);
lat = StrictMath.toRadians(lat);
lon = TO_RADIANS * lon;
lat = TO_RADIANS * lat;
final double sl = StrictMath.sin(lat);
final double sl = SloppyMath.sin(lat);
final double s2 = sl*sl;
final double cl = StrictMath.cos(lat);
final double ge2 = (SEMIMAJOR_AXIS2 - SEMIMINOR_AXIS2)/(SEMIMAJOR_AXIS2);
final double cl = SloppyMath.cos(lat);
if (ecf == null) {
ecf = new double[3];
}
if (lat < -PI_OVER_2 && lat > -1.001D * PI_OVER_2) {
lat = -PI_OVER_2;
} else if (lat > PI_OVER_2 && lat < 1.001D * PI_OVER_2) {
lat = PI_OVER_2;
if (lat < -PIO2 && lat > -1.001D * PIO2) {
lat = -PIO2;
} else if (lat > PIO2 && lat < 1.001D * PIO2) {
lat = PIO2;
}
assert (lat >= -PI_OVER_2) || (lat <= PI_OVER_2);
assert (lat >= -PIO2) || (lat <= PIO2);
if (lon > StrictMath.PI) {
lon -= (2*StrictMath.PI);
}
final double rn = SEMIMAJOR_AXIS / StrictMath.sqrt(1.0D - ge2 * s2);
ecf[0] = (rn+alt) * cl * StrictMath.cos(lon);
ecf[1] = (rn+alt) * cl * StrictMath.sin(lon);
ecf[2] = ((rn*(1.0-ge2))+alt)*sl;
final double rn = SEMIMAJOR_AXIS / StrictMath.sqrt(1.0D - E2 * s2);
ecf[0] = (rn+alt) * cl * SloppyMath.cos(lon);
ecf[1] = (rn+alt) * cl * SloppyMath.sin(lon);
ecf[2] = ((rn*(1.0-E2))+alt)*sl;
return ecf;
}
@ -272,13 +274,13 @@ public class GeoProjectionUtils {
phiMatrix = new double[3][3];
}
originLon = StrictMath.toRadians(originLon);
originLat = StrictMath.toRadians(originLat);
originLon = TO_RADIANS * originLon;
originLat = TO_RADIANS * originLat;
final double sLon = StrictMath.sin(originLon);
final double cLon = StrictMath.cos(originLon);
final double sLat = StrictMath.sin(originLat);
final double cLat = StrictMath.cos(originLat);
final double sLon = SloppyMath.sin(originLon);
final double cLon = SloppyMath.cos(originLon);
final double sLat = SloppyMath.sin(originLat);
final double cLat = SloppyMath.cos(originLat);
phiMatrix[0][0] = -sLon;
phiMatrix[0][1] = cLon;
@ -306,13 +308,13 @@ public class GeoProjectionUtils {
phiMatrix = new double[3][3];
}
originLon = StrictMath.toRadians(originLon);
originLat = StrictMath.toRadians(originLat);
originLon = TO_RADIANS * originLon;
originLat = TO_RADIANS * originLat;
final double sLat = StrictMath.sin(originLat);
final double cLat = StrictMath.cos(originLat);
final double sLon = StrictMath.sin(originLon);
final double cLon = StrictMath.cos(originLon);
final double sLat = SloppyMath.sin(originLat);
final double cLat = SloppyMath.cos(originLat);
final double sLon = SloppyMath.sin(originLon);
final double cLon = SloppyMath.cos(originLon);
phiMatrix[0][0] = -sLon;
phiMatrix[1][0] = cLon;
@ -337,22 +339,22 @@ public class GeoProjectionUtils {
* @param pt resulting point
* @return the point along a bearing at a given distance in meters
*/
public static final double[] pointFromLonLatBearing(double lon, double lat, double bearing, double dist, double[] pt) {
public static final double[] pointFromLonLatBearingVincenty(double lon, double lat, double bearing, double dist, double[] pt) {
if (pt == null) {
pt = new double[2];
}
final double alpha1 = StrictMath.toRadians(bearing);
final double cosA1 = StrictMath.cos(alpha1);
final double sinA1 = StrictMath.sin(alpha1);
final double tanU1 = (1-FLATTENING) * StrictMath.tan(StrictMath.toRadians(lat));
final double alpha1 = TO_RADIANS * bearing;
final double cosA1 = SloppyMath.cos(alpha1);
final double sinA1 = SloppyMath.sin(alpha1);
final double tanU1 = (1-FLATTENING) * SloppyMath.tan(TO_RADIANS * lat);
final double cosU1 = 1 / StrictMath.sqrt((1+tanU1*tanU1));
final double sinU1 = tanU1*cosU1;
final double sig1 = StrictMath.atan2(tanU1, cosA1);
final double sinAlpha = cosU1 * sinA1;
final double cosSqAlpha = 1 - sinAlpha*sinAlpha;
final double uSq = cosSqAlpha * (SEMIMAJOR_AXIS2 - SEMIMINOR_AXIS2) / SEMIMINOR_AXIS2;
final double uSq = cosSqAlpha * EP2;
final double A = 1 + uSq/16384D*(4096D + uSq * (-768D + uSq * (320D - 175D*uSq)));
final double B = uSq/1024D * (256D + uSq * (-128D + uSq * (74D - 47D * uSq)));
@ -361,9 +363,9 @@ public class GeoProjectionUtils {
double sinSigma, cosSigma, cos2SigmaM, deltaSigma;
do {
cos2SigmaM = StrictMath.cos(2*sig1 + sigma);
sinSigma = StrictMath.sin(sigma);
cosSigma = StrictMath.cos(sigma);
cos2SigmaM = SloppyMath.cos(2*sig1 + sigma);
sinSigma = SloppyMath.sin(sigma);
cosSigma = SloppyMath.cos(sigma);
deltaSigma = B * sinSigma * (cos2SigmaM + (B/4D) * (cosSigma*(-1+2*cos2SigmaM*cos2SigmaM)-
(B/6) * cos2SigmaM*(-3+4*sinSigma*sinSigma)*(-3+4*cos2SigmaM*cos2SigmaM)));
@ -379,9 +381,42 @@ public class GeoProjectionUtils {
final double lam = lambda - (1-c) * FLATTENING * sinAlpha *
(sigma + c * sinSigma * (cos2SigmaM + c * cosSigma * (-1 + 2* cos2SigmaM*cos2SigmaM)));
pt[0] = GeoUtils.normalizeLon(lon + StrictMath.toDegrees(lam));
pt[1] = GeoUtils.normalizeLat(StrictMath.toDegrees(lat2));
pt[0] = GeoUtils.normalizeLon(lon + TO_DEGREES * lam);
pt[1] = GeoUtils.normalizeLat(TO_DEGREES * lat2);
return pt;
}
/**
* Finds a point along a bearing from a given lon,lat geolocation using great circle arc
*
* @param lon origin longitude in degrees
* @param lat origin latitude in degrees
* @param bearing azimuthal bearing in degrees
* @param dist distance in meters
* @param pt resulting point
* @return the point along a bearing at a given distance in meters
*/
public static final double[] pointFromLonLatBearingGreatCircle(double lon, double lat, double bearing, double dist, double[] pt) {
if (pt == null) {
pt = new double[2];
}
lon *= TO_RADIANS;
lat *= TO_RADIANS;
bearing *= TO_RADIANS;
final double cLat = SloppyMath.cos(lat);
final double sLat = SloppyMath.sin(lat);
final double sinDoR = SloppyMath.sin(dist / GeoProjectionUtils.SEMIMAJOR_AXIS);
final double cosDoR = SloppyMath.cos(dist / GeoProjectionUtils.SEMIMAJOR_AXIS);
pt[1] = SloppyMath.asin(sLat*cosDoR + cLat * sinDoR * SloppyMath.cos(bearing));
pt[0] = TO_DEGREES * (lon + Math.atan2(SloppyMath.sin(bearing) * sinDoR * cLat, cosDoR - sLat * SloppyMath.sin(pt[1])));
pt[1] *= TO_DEGREES;
return pt;
}
}

View File

@ -0,0 +1,356 @@
package org.apache.lucene.util;
/*
* 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.
*/
/**
* Reusable geo-relation utility methods
*/
public class GeoRelationUtils {
/**
* Determine if a bbox (defined by minLon, minLat, maxLon, maxLat) contains the provided point (defined by lon, lat)
* NOTE: this is a basic method that does not handle dateline or pole crossing. Unwrapping must be done before
* calling this method.
*/
public static boolean pointInRect(final double lon, final double lat, final double minLon,
final double minLat, final double maxLon, final double maxLat) {
return (GeoUtils.compare(lon, minLon) >= 0 && GeoUtils.compare(lon, maxLon) <= 0
&& GeoUtils.compare(lat, minLat) >= 0 && GeoUtils.compare(lat, maxLat) <= 0);
}
/**
* simple even-odd point in polygon computation
* 1. Determine if point is contained in the longitudinal range
* 2. Determine whether point crosses the edge by computing the latitudinal delta
* between the end-point of a parallel vector (originating at the point) and the
* y-component of the edge sink
*
* NOTE: Requires polygon point (x,y) order either clockwise or counter-clockwise
*/
public static boolean pointInPolygon(double[] x, double[] y, double lat, double lon) {
assert x.length == y.length;
boolean inPoly = false;
/**
* Note: This is using a euclidean coordinate system which could result in
* upwards of 110KM error at the equator.
* TODO convert coordinates to cylindrical projection (e.g. mercator)
*/
for (int i = 1; i < x.length; i++) {
if (x[i] < lon && x[i-1] >= lon || x[i-1] < lon && x[i] >= lon) {
if (y[i] + (lon - x[i]) / (x[i-1] - x[i]) * (y[i-1] - y[i]) < lat) {
inPoly = !inPoly;
}
}
}
return inPoly;
}
public static boolean rectDisjoint(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY,
final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) {
return (aMaxX < bMinX || aMinX > bMaxX || aMaxY < bMinY || aMinY > bMaxY);
}
/**
* Computes whether the first (a) rectangle is wholly within another (b) rectangle (shared boundaries allowed)
*/
public static boolean rectWithin(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY,
final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) {
return !(aMinX < bMinX || aMinY < bMinY || aMaxX > bMaxX || aMaxY > bMaxY);
}
public static boolean rectCrosses(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY,
final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) {
return !(rectDisjoint(aMinX, aMinY, aMaxX, aMaxY, bMinX, bMinY, bMaxX, bMaxY) ||
rectWithin(aMinX, aMinY, aMaxX, aMaxY, bMinX, bMinY, bMaxX, bMaxY));
}
/**
* Computes whether rectangle a contains rectangle b (touching allowed)
*/
public static boolean rectContains(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY,
final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) {
return !(bMinX < aMinX || bMinY < aMinY || bMaxX > aMaxX || bMaxY > aMaxY);
}
/**
* Computes whether a rectangle intersects another rectangle (crosses, within, touching, etc)
*/
public static boolean rectIntersects(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY,
final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) {
return !((aMaxX < bMinX || aMinX > bMaxX || aMaxY < bMinY || aMinY > bMaxY) );
}
/**
* Computes whether a rectangle crosses a shape. (touching not allowed)
*/
public static boolean rectCrossesPoly(final double rMinX, final double rMinY, final double rMaxX,
final double rMaxY, final double[] shapeX, final double[] shapeY,
final double sMinX, final double sMinY, final double sMaxX,
final double sMaxY) {
// short-circuit: if the bounding boxes are disjoint then the shape does not cross
if (rectDisjoint(rMinX, rMinY, rMaxX, rMaxY, sMinX, sMinY, sMaxX, sMaxY)) {
return false;
}
final double[][] bbox = new double[][] { {rMinX, rMinY}, {rMaxX, rMinY}, {rMaxX, rMaxY}, {rMinX, rMaxY}, {rMinX, rMinY} };
final int polyLength = shapeX.length-1;
double d, s, t, a1, b1, c1, a2, b2, c2;
double x00, y00, x01, y01, x10, y10, x11, y11;
// computes the intersection point between each bbox edge and the polygon edge
for (short b=0; b<4; ++b) {
a1 = bbox[b+1][1]-bbox[b][1];
b1 = bbox[b][0]-bbox[b+1][0];
c1 = a1*bbox[b+1][0] + b1*bbox[b+1][1];
for (int p=0; p<polyLength; ++p) {
a2 = shapeY[p+1]-shapeY[p];
b2 = shapeX[p]-shapeX[p+1];
// compute determinant
d = a1*b2 - a2*b1;
if (d != 0) {
// lines are not parallel, check intersecting points
c2 = a2*shapeX[p+1] + b2*shapeY[p+1];
s = (1/d)*(b2*c1 - b1*c2);
t = (1/d)*(a1*c2 - a2*c1);
x00 = StrictMath.min(bbox[b][0], bbox[b+1][0]) - GeoUtils.TOLERANCE;
x01 = StrictMath.max(bbox[b][0], bbox[b+1][0]) + GeoUtils.TOLERANCE;
y00 = StrictMath.min(bbox[b][1], bbox[b+1][1]) - GeoUtils.TOLERANCE;
y01 = StrictMath.max(bbox[b][1], bbox[b+1][1]) + GeoUtils.TOLERANCE;
x10 = StrictMath.min(shapeX[p], shapeX[p+1]) - GeoUtils.TOLERANCE;
x11 = StrictMath.max(shapeX[p], shapeX[p+1]) + GeoUtils.TOLERANCE;
y10 = StrictMath.min(shapeY[p], shapeY[p+1]) - GeoUtils.TOLERANCE;
y11 = StrictMath.max(shapeY[p], shapeY[p+1]) + GeoUtils.TOLERANCE;
// check whether the intersection point is touching one of the line segments
boolean touching = ((x00 == s && y00 == t) || (x01 == s && y01 == t))
|| ((x10 == s && y10 == t) || (x11 == s && y11 == t));
// if line segments are not touching and the intersection point is within the range of either segment
if (!(touching || x00 > s || x01 < s || y00 > t || y01 < t || x10 > s || x11 < s || y10 > t || y11 < t)) {
return true;
}
}
} // for each poly edge
} // for each bbox edge
return false;
}
/**
* Computes whether a rectangle is within a given polygon (shared boundaries allowed)
*/
public static boolean rectWithinPoly(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
final double[] shapeX, final double[] shapeY, final double sMinX,
final double sMinY, final double sMaxX, final double sMaxY) {
// check if rectangle crosses poly (to handle concave/pacman polys), then check that all 4 corners
// are contained
return !(rectCrossesPoly(rMinX, rMinY, rMaxX, rMaxY, shapeX, shapeY, sMinX, sMinY, sMaxX, sMaxY) ||
!pointInPolygon(shapeX, shapeY, rMinY, rMinX) || !pointInPolygon(shapeX, shapeY, rMinY, rMaxX) ||
!pointInPolygon(shapeX, shapeY, rMaxY, rMaxX) || !pointInPolygon(shapeX, shapeY, rMaxY, rMinX));
}
private static boolean rectAnyCornersInCircle(final double rMinX, final double rMinY, final double rMaxX,
final double rMaxY, final double centerLon, final double centerLat,
final double radiusMeters, final boolean approx) {
if (approx == true) {
return rectAnyCornersInCircleSloppy(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, radiusMeters);
}
double w = Math.abs(rMaxX - rMinX);
if (w <= 90.0) {
return GeoDistanceUtils.haversin(centerLat, centerLon, rMinY, rMinX) <= radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMaxY, rMinX) <= radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMaxY, rMaxX) <= radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMinY, rMaxX) <= radiusMeters;
}
// partition
w /= 4;
final double p1 = rMinX + w;
final double p2 = p1 + w;
final double p3 = p2 + w;
return GeoDistanceUtils.haversin(centerLat, centerLon, rMinY, rMinX) <= radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMaxY, rMinX) <= radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMaxY, p1) <= radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMinY, p1) <= radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMinY, p2) <= radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMaxY, p2) <= radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMaxY, p3) <= radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMinY, p3) <= radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMaxY, rMaxX) <= radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMinY, rMaxX) <= radiusMeters;
}
private static boolean rectAnyCornersInCircleSloppy(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
final double centerLon, final double centerLat, final double radiusMeters) {
return SloppyMath.haversin(centerLat, centerLon, rMinY, rMinX)*1000.0 <= radiusMeters
|| SloppyMath.haversin(centerLat, centerLon, rMaxY, rMinX)*1000.0 <= radiusMeters
|| SloppyMath.haversin(centerLat, centerLon, rMaxY, rMaxX)*1000.0 <= radiusMeters
|| SloppyMath.haversin(centerLat, centerLon, rMinY, rMaxX)*1000.0 <= radiusMeters;
}
private static boolean rectAnyCornersOutsideCircle(final double rMinX, final double rMinY, final double rMaxX,
final double rMaxY, final double centerLon, final double centerLat,
final double radiusMeters, final boolean approx) {
if (approx == true) {
return rectAnyCornersOutsideCircleSloppy(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, radiusMeters);
}
double w = Math.abs(rMaxX - rMinX);
if (w <= 90.0) {
return GeoDistanceUtils.haversin(centerLat, centerLon, rMinY, rMinX) > radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMaxY, rMinX) > radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMaxY, rMaxX) > radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMinY, rMaxX) > radiusMeters;
}
// partition
w /= 4;
final double p1 = rMinX + w;
final double p2 = p1 + w;
final double p3 = p2 + w;
return GeoDistanceUtils.haversin(centerLat, centerLon, rMinY, rMinX) > radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMaxY, rMinX) > radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMaxY, p1) > radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMinY, p1) > radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMinY, p2) > radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMaxY, p2) > radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMaxY, p3) > radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMinY, p3) > radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMaxY, rMaxX) > radiusMeters
|| GeoDistanceUtils.haversin(centerLat, centerLon, rMinY, rMaxX) > radiusMeters;
}
private static boolean rectAnyCornersOutsideCircleSloppy(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
final double centerLon, final double centerLat, final double radiusMeters) {
return SloppyMath.haversin(centerLat, centerLon, rMinY, rMinX)*1000.0 > radiusMeters
|| SloppyMath.haversin(centerLat, centerLon, rMaxY, rMinX)*1000.0 > radiusMeters
|| SloppyMath.haversin(centerLat, centerLon, rMaxY, rMaxX)*1000.0 > radiusMeters
|| SloppyMath.haversin(centerLat, centerLon, rMinY, rMaxX)*1000.0 > radiusMeters;
}
public static boolean rectWithinCircle(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
final double centerLon, final double centerLat, final double radiusMeters) {
return rectWithinCircle(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, radiusMeters, false);
}
public static boolean rectWithinCircle(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
final double centerLon, final double centerLat, final double radiusMeters,
final boolean approx) {
return rectAnyCornersOutsideCircle(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, radiusMeters, approx) == false;
}
/**
* Determine if a bbox (defined by minLon, minLat, maxLon, maxLat) contains the provided point (defined by lon, lat)
* NOTE: this is basic method that does not handle dateline or pole crossing. Unwrapping must be done before
* calling this method.
*/
public static boolean rectCrossesCircle(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
final double centerLon, final double centerLat, final double radiusMeters) {
return rectCrossesCircle(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, radiusMeters, false);
}
public static boolean rectCrossesCircle(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
final double centerLon, final double centerLat, final double radiusMeters,
final boolean approx) {
if (approx == true) {
return rectAnyCornersInCircle(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, radiusMeters, approx)
|| isClosestPointOnRectWithinRange(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, radiusMeters, approx);
}
return (rectAnyCornersInCircle(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, radiusMeters, approx) &&
rectAnyCornersOutsideCircle(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, radiusMeters, approx))
|| isClosestPointOnRectWithinRange(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, radiusMeters, approx);
}
private static boolean isClosestPointOnRectWithinRange(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
final double centerLon, final double centerLat, final double radiusMeters,
final boolean approx) {
double[] closestPt = {0, 0};
GeoDistanceUtils.closestPointOnBBox(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, closestPt);
boolean haverShortCut = GeoDistanceUtils.haversin(centerLat, centerLon, closestPt[1], closestPt[0]) <= radiusMeters;
if (approx == true || haverShortCut == true) {
return haverShortCut;
}
double lon1 = rMinX;
double lon2 = rMaxX;
double lat1 = rMinY;
double lat2 = rMaxY;
if (closestPt[0] == rMinX || closestPt[0] == rMaxX) {
lon1 = closestPt[0];
lon2 = lon1;
} else if (closestPt[1] == rMinY || closestPt[1] == rMaxY) {
lat1 = closestPt[1];
lat2 = lat1;
}
return lineCrossesSphere(lon1, lat1, 0, lon2, lat2, 0, centerLon, centerLat, 0, radiusMeters);
}
/**
* Computes whether or a 3dimensional line segment intersects or crosses a sphere
*
* @param lon1 longitudinal location of the line segment start point (in degrees)
* @param lat1 latitudinal location of the line segment start point (in degrees)
* @param alt1 altitude of the line segment start point (in degrees)
* @param lon2 longitudinal location of the line segment end point (in degrees)
* @param lat2 latitudinal location of the line segment end point (in degrees)
* @param alt2 altitude of the line segment end point (in degrees)
* @param centerLon longitudinal location of center search point (in degrees)
* @param centerLat latitudinal location of center search point (in degrees)
* @param centerAlt altitude of the center point (in meters)
* @param radiusMeters search sphere radius (in meters)
* @return whether the provided line segment is a secant of the
*/
private static boolean lineCrossesSphere(double lon1, double lat1, double alt1, double lon2,
double lat2, double alt2, double centerLon, double centerLat,
double centerAlt, double radiusMeters) {
// convert to cartesian 3d (in meters)
double[] ecf1 = GeoProjectionUtils.llaToECF(lon1, lat1, alt1, null);
double[] ecf2 = GeoProjectionUtils.llaToECF(lon2, lat2, alt2, null);
double[] cntr = GeoProjectionUtils.llaToECF(centerLon, centerLat, centerAlt, null);
// convert radius from arc radius to cartesian radius
double[] oneEighty = GeoProjectionUtils.pointFromLonLatBearingGreatCircle(centerLon, centerLat, 180.0d, radiusMeters, new double[3]);
GeoProjectionUtils.llaToECF(oneEighty[0], oneEighty[1], 0, oneEighty);
radiusMeters = GeoDistanceUtils.linearDistance(oneEighty, cntr);// Math.sqrt(oneEighty[0]*cntr[0] + oneEighty[1]*cntr[1] + oneEighty[2]*cntr[2]);
final double dX = ecf2[0] - ecf1[0];
final double dY = ecf2[1] - ecf1[1];
final double dZ = ecf2[2] - ecf1[2];
final double fX = ecf1[0] - cntr[0];
final double fY = ecf1[1] - cntr[1];
final double fZ = ecf1[2] - cntr[2];
final double a = dX*dX + dY*dY + dZ*dZ;
final double b = 2 * (fX*dX + fY*dY + fZ*dZ);
final double c = (fX*fX + fY*fY + fZ*fZ) - (radiusMeters*radiusMeters);
double discrim = (b*b)-(4*a*c);
if (discrim < 0) {
return false;
}
discrim = StrictMath.sqrt(discrim);
final double a2 = 2*a;
final double t1 = (-b - discrim)/a2;
final double t2 = (-b + discrim)/a2;
if ( (t1 < 0 || t1 > 1) ) {
return !(t2 < 0 || t2 > 1);
}
return true;
}
}

View File

@ -19,6 +19,9 @@ package org.apache.lucene.util;
import java.util.ArrayList;
import static org.apache.lucene.util.SloppyMath.TO_DEGREES;
import static org.apache.lucene.util.SloppyMath.TO_RADIANS;
/**
* Basic reusable geo-spatial utility methods
*
@ -74,6 +77,9 @@ public final class GeoUtils {
return (val / LAT_SCALE) + MIN_LAT_INCL;
}
/**
* Compare two position values within a {@link org.apache.lucene.util.GeoUtils#TOLERANCE} factor
*/
public static double compare(final double v1, final double v2) {
final double delta = v1-v2;
return Math.abs(delta) <= TOLERANCE ? 0 : delta;
@ -107,44 +113,6 @@ public final class GeoUtils {
return (off <= 180 ? off : 360-off) - 90;
}
/**
* Determine if a bbox (defined by minLon, minLat, maxLon, maxLat) contains the provided point (defined by lon, lat)
* NOTE: this is a basic method that does not handle dateline or pole crossing. Unwrapping must be done before
* calling this method.
*/
public static boolean bboxContains(final double lon, final double lat, final double minLon,
final double minLat, final double maxLon, final double maxLat) {
return (compare(lon, minLon) >= 0 && compare(lon, maxLon) <= 0
&& compare(lat, minLat) >= 0 && compare(lat, maxLat) <= 0);
}
/**
* simple even-odd point in polygon computation
* 1. Determine if point is contained in the longitudinal range
* 2. Determine whether point crosses the edge by computing the latitudinal delta
* between the end-point of a parallel vector (originating at the point) and the
* y-component of the edge sink
*
* NOTE: Requires polygon point (x,y) order either clockwise or counter-clockwise
*/
public static boolean pointInPolygon(double[] x, double[] y, double lat, double lon) {
assert x.length == y.length;
boolean inPoly = false;
/**
* Note: This is using a euclidean coordinate system which could result in
* upwards of 110KM error at the equator.
* TODO convert coordinates to cylindrical projection (e.g. mercator)
*/
for (int i = 1; i < x.length; i++) {
if (x[i] < lon && x[i-1] >= lon || x[i-1] < lon && x[i] >= lon) {
if (y[i] + (lon - x[i]) / (x[i-1] - x[i]) * (y[i-1] - y[i]) < lat) {
inPoly = !inPoly;
}
}
}
return inPoly;
}
public static String geoTermToString(long term) {
StringBuilder s = new StringBuilder(64);
final int numberOfLeadingZeros = Long.numberOfLeadingZeros(term);
@ -157,95 +125,6 @@ public final class GeoUtils {
return s.toString();
}
public static boolean rectDisjoint(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY,
final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) {
return (aMaxX < bMinX || aMinX > bMaxX || aMaxY < bMinY || aMinY > bMaxY);
}
/**
* Computes whether the first (a) rectangle is wholly within another (b) rectangle (shared boundaries allowed)
*/
public static boolean rectWithin(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY,
final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) {
return !(aMinX < bMinX || aMinY < bMinY || aMaxX > bMaxX || aMaxY > bMaxY);
}
public static boolean rectCrosses(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY,
final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) {
return !(rectDisjoint(aMinX, aMinY, aMaxX, aMaxY, bMinX, bMinY, bMaxX, bMaxY) ||
rectWithin(aMinX, aMinY, aMaxX, aMaxY, bMinX, bMinY, bMaxX, bMaxY));
}
/**
* Computes whether rectangle a contains rectangle b (touching allowed)
*/
public static boolean rectContains(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY,
final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) {
return !(bMinX < aMinX || bMinY < aMinY || bMaxX > aMaxX || bMaxY > aMaxY);
}
/**
* Computes whether a rectangle intersects another rectangle (crosses, within, touching, etc)
*/
public static boolean rectIntersects(final double aMinX, final double aMinY, final double aMaxX, final double aMaxY,
final double bMinX, final double bMinY, final double bMaxX, final double bMaxY) {
return !((aMaxX < bMinX || aMinX > bMaxX || aMaxY < bMinY || aMinY > bMaxY) );
}
/**
* Computes whether a rectangle crosses a shape. (touching not allowed)
*/
public static boolean rectCrossesPoly(final double rMinX, final double rMinY, final double rMaxX,
final double rMaxY, final double[] shapeX, final double[] shapeY,
final double sMinX, final double sMinY, final double sMaxX,
final double sMaxY) {
// short-circuit: if the bounding boxes are disjoint then the shape does not cross
if (rectDisjoint(rMinX, rMinY, rMaxX, rMaxY, sMinX, sMinY, sMaxX, sMaxY)) {
return false;
}
final double[][] bbox = new double[][] { {rMinX, rMinY}, {rMaxX, rMinY}, {rMaxX, rMaxY}, {rMinX, rMaxY}, {rMinX, rMinY} };
final int polyLength = shapeX.length-1;
double d, s, t, a1, b1, c1, a2, b2, c2;
double x00, y00, x01, y01, x10, y10, x11, y11;
// computes the intersection point between each bbox edge and the polygon edge
for (short b=0; b<4; ++b) {
a1 = bbox[b+1][1]-bbox[b][1];
b1 = bbox[b][0]-bbox[b+1][0];
c1 = a1*bbox[b+1][0] + b1*bbox[b+1][1];
for (int p=0; p<polyLength; ++p) {
a2 = shapeY[p+1]-shapeY[p];
b2 = shapeX[p]-shapeX[p+1];
// compute determinant
d = a1*b2 - a2*b1;
if (d != 0) {
// lines are not parallel, check intersecting points
c2 = a2*shapeX[p+1] + b2*shapeY[p+1];
s = (1/d)*(b2*c1 - b1*c2);
t = (1/d)*(a1*c2 - a2*c1);
x00 = StrictMath.min(bbox[b][0], bbox[b+1][0]) - TOLERANCE;
x01 = StrictMath.max(bbox[b][0], bbox[b+1][0]) + TOLERANCE;
y00 = StrictMath.min(bbox[b][1], bbox[b+1][1]) - TOLERANCE;
y01 = StrictMath.max(bbox[b][1], bbox[b+1][1]) + TOLERANCE;
x10 = StrictMath.min(shapeX[p], shapeX[p+1]) - TOLERANCE;
x11 = StrictMath.max(shapeX[p], shapeX[p+1]) + TOLERANCE;
y10 = StrictMath.min(shapeY[p], shapeY[p+1]) - TOLERANCE;
y11 = StrictMath.max(shapeY[p], shapeY[p+1]) + TOLERANCE;
// check whether the intersection point is touching one of the line segments
boolean touching = ((x00 == s && y00 == t) || (x01 == s && y01 == t))
|| ((x10 == s && y10 == t) || (x11 == s && y11 == t));
// if line segments are not touching and the intersection point is within the range of either segment
if (!(touching || x00 > s || x01 < s || y00 > t || y01 < t || x10 > s || x11 < s || y10 > t || y11 < t)) {
return true;
}
}
} // for each poly edge
} // for each bbox edge
return false;
}
/**
* Converts a given circle (defined as a point/radius) to an approximated line-segment polygon
*
@ -267,7 +146,7 @@ public final class GeoUtils {
final int sidesLen = sides-1;
for (int i=0; i<sidesLen; ++i) {
angle = (i*360/sides);
pt = GeoProjectionUtils.pointFromLonLatBearing(lon, lat, angle, radiusMeters, pt);
pt = GeoProjectionUtils.pointFromLonLatBearingGreatCircle(lon, lat, angle, radiusMeters, pt);
lons[i] = pt[0];
lats[i] = pt[1];
}
@ -280,64 +159,12 @@ public final class GeoUtils {
return geometry;
}
/**
* Computes whether a rectangle is within a given polygon (shared boundaries allowed)
*/
public static boolean rectWithinPoly(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
final double[] shapeX, final double[] shapeY, final double sMinX,
final double sMinY, final double sMaxX, final double sMaxY) {
// check if rectangle crosses poly (to handle concave/pacman polys), then check that all 4 corners
// are contained
return !(rectCrossesPoly(rMinX, rMinY, rMaxX, rMaxY, shapeX, shapeY, sMinX, sMinY, sMaxX, sMaxY) ||
!pointInPolygon(shapeX, shapeY, rMinY, rMinX) || !pointInPolygon(shapeX, shapeY, rMinY, rMaxX) ||
!pointInPolygon(shapeX, shapeY, rMaxY, rMaxX) || !pointInPolygon(shapeX, shapeY, rMaxY, rMinX));
}
private static boolean rectAnyCornersOutsideCircle(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
final double centerLon, final double centerLat, final double radiusMeters) {
return SloppyMath.haversin(centerLat, centerLon, rMinY, rMinX)*1000.0 > radiusMeters
|| SloppyMath.haversin(centerLat, centerLon, rMaxY, rMinX)*1000.0 > radiusMeters
|| SloppyMath.haversin(centerLat, centerLon, rMaxY, rMaxX)*1000.0 > radiusMeters
|| SloppyMath.haversin(centerLat, centerLon, rMinY, rMaxX)*1000.0 > radiusMeters;
}
private static boolean rectAnyCornersInCircle(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
final double centerLon, final double centerLat, final double radiusMeters) {
return SloppyMath.haversin(centerLat, centerLon, rMinY, rMinX)*1000.0 <= radiusMeters
|| SloppyMath.haversin(centerLat, centerLon, rMaxY, rMinX)*1000.0 <= radiusMeters
|| SloppyMath.haversin(centerLat, centerLon, rMaxY, rMaxX)*1000.0 <= radiusMeters
|| SloppyMath.haversin(centerLat, centerLon, rMinY, rMaxX)*1000.0 <= radiusMeters;
}
public static boolean rectWithinCircle(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
final double centerLon, final double centerLat, final double radiusMeters) {
return rectAnyCornersOutsideCircle(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, radiusMeters) == false;
}
/**
* Determine if a bbox (defined by minLon, minLat, maxLon, maxLat) contains the provided point (defined by lon, lat)
* NOTE: this is basic method that does not handle dateline or pole crossing. Unwrapping must be done before
* calling this method.
*/
public static boolean rectCrossesCircle(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
final double centerLon, final double centerLat, final double radiusMeters) {
return rectAnyCornersInCircle(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, radiusMeters)
|| isClosestPointOnRectWithinRange(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, radiusMeters);
}
private static boolean isClosestPointOnRectWithinRange(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
final double centerLon, final double centerLat, final double radiusMeters) {
double[] closestPt = {0, 0};
GeoDistanceUtils.closestPointOnBBox(rMinX, rMinY, rMaxX, rMaxY, centerLon, centerLat, closestPt);
return SloppyMath.haversin(centerLat, centerLon, closestPt[1], closestPt[0])*1000.0 <= radiusMeters;
}
/**
* Compute Bounding Box for a circle using WGS-84 parameters
*/
public static GeoRect circleToBBox(final double centerLon, final double centerLat, final double radiusMeters) {
final double radLat = StrictMath.toRadians(centerLat);
final double radLon = StrictMath.toRadians(centerLon);
final double radLat = TO_RADIANS * centerLat;
final double radLon = TO_RADIANS * centerLon;
double radDistance = radiusMeters / GeoProjectionUtils.SEMIMAJOR_AXIS;
double minLat = radLat - radDistance;
double maxLat = radLat + radDistance;
@ -345,7 +172,7 @@ public final class GeoUtils {
double maxLon;
if (minLat > GeoProjectionUtils.MIN_LAT_RADIANS && maxLat < GeoProjectionUtils.MAX_LAT_RADIANS) {
double deltaLon = StrictMath.asin(StrictMath.sin(radDistance) / StrictMath.cos(radLat));
double deltaLon = SloppyMath.asin(SloppyMath.sin(radDistance) / SloppyMath.cos(radLat));
minLon = radLon - deltaLon;
if (minLon < GeoProjectionUtils.MIN_LON_RADIANS) {
minLon += 2d * StrictMath.PI;
@ -362,10 +189,12 @@ public final class GeoUtils {
maxLon = GeoProjectionUtils.MAX_LON_RADIANS;
}
return new GeoRect(StrictMath.toDegrees(minLon), StrictMath.toDegrees(maxLon),
StrictMath.toDegrees(minLat), StrictMath.toDegrees(maxLat));
return new GeoRect(TO_DEGREES * minLon, TO_DEGREES * maxLon, TO_DEGREES * minLat, TO_DEGREES * maxLat);
}
/**
* Compute Bounding Box for a polygon using WGS-84 parameters
*/
public static GeoRect polyToBBox(double[] polyLons, double[] polyLats) {
if (polyLons.length != polyLats.length) {
throw new IllegalArgumentException("polyLons and polyLats must be equal length");
@ -393,64 +222,16 @@ public final class GeoUtils {
GeoUtils.unscaleLat(GeoUtils.scaleLat(minLat)), GeoUtils.unscaleLat(GeoUtils.scaleLat(maxLat)));
}
/*
/**
* Computes whether or a 3dimensional line segment intersects or crosses a sphere
*
* @param lon1 longitudinal location of the line segment start point (in degrees)
* @param lat1 latitudinal location of the line segment start point (in degrees)
* @param alt1 altitude of the line segment start point (in degrees)
* @param lon2 longitudinal location of the line segment end point (in degrees)
* @param lat2 latitudinal location of the line segment end point (in degrees)
* @param alt2 altitude of the line segment end point (in degrees)
* @param centerLon longitudinal location of center search point (in degrees)
* @param centerLat latitudinal location of center search point (in degrees)
* @param centerAlt altitude of the center point (in meters)
* @param radiusMeters search sphere radius (in meters)
* @return whether the provided line segment is a secant of the
* /
// NOTE: not used for 2d at the moment. used for 3d w/ altitude (we can keep or add back)
private static boolean lineCrossesSphere(double lon1, double lat1, double alt1, double lon2,
double lat2, double alt2, double centerLon, double centerLat,
double centerAlt, double radiusMeters) {
// convert to cartesian 3d (in meters)
double[] ecf1 = GeoProjectionUtils.llaToECF(lon1, lat1, alt1, null);
double[] ecf2 = GeoProjectionUtils.llaToECF(lon2, lat2, alt2, null);
double[] cntr = GeoProjectionUtils.llaToECF(centerLon, centerLat, centerAlt, null);
final double dX = ecf2[0] - ecf1[0];
final double dY = ecf2[1] - ecf1[1];
final double dZ = ecf2[2] - ecf1[2];
final double fX = ecf1[0] - cntr[0];
final double fY = ecf1[1] - cntr[1];
final double fZ = ecf1[2] - cntr[2];
final double a = dX*dX + dY*dY + dZ*dZ;
final double b = 2 * (fX*dX + fY*dY + fZ*dZ);
final double c = (fX*fX + fY*fY + fZ*fZ) - (radiusMeters*radiusMeters);
double discrim = (b*b)-(4*a*c);
if (discrim < 0) {
return false;
}
discrim = StrictMath.sqrt(discrim);
final double a2 = 2*a;
final double t1 = (-b - discrim)/a2;
final double t2 = (-b + discrim)/a2;
if ( (t1 < 0 || t1 > 1) ) {
return !(t2 < 0 || t2 > 1);
}
return true;
}
*/
* validates latitude value is within standard +/-90 coordinate bounds
*/
public static boolean isValidLat(double lat) {
return Double.isNaN(lat) == false && lat >= MIN_LAT_INCL && lat <= MAX_LAT_INCL;
}
/**
* validates longitude value is within standard +/-180 coordinate bounds
*/
public static boolean isValidLon(double lon) {
return Double.isNaN(lon) == false && lon >= MIN_LON_INCL && lon <= MAX_LON_INCL;
}

View File

@ -21,6 +21,7 @@ import org.apache.lucene.document.DimensionalLatLonField;
import org.apache.lucene.document.Document;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.BaseGeoPointTestCase;
import org.apache.lucene.util.GeoDistanceUtils;
import org.apache.lucene.util.GeoRect;
import org.apache.lucene.util.SloppyMath;
@ -96,15 +97,15 @@ public class TestDimensionalQueries extends BaseGeoPointTestCase {
@Override
protected Boolean circleContainsPoint(double centerLat, double centerLon, double radiusMeters, double pointLat, double pointLon) {
double distanceKM = SloppyMath.haversin(centerLat, centerLon, pointLat, pointLon);
boolean result = distanceKM*1000.0 <= radiusMeters;
double distanceMeters = GeoDistanceUtils.haversin(centerLat, centerLon, pointLat, pointLon);
boolean result = distanceMeters <= radiusMeters;
//System.out.println(" shouldMatch? centerLon=" + centerLon + " centerLat=" + centerLat + " pointLon=" + pointLon + " pointLat=" + pointLat + " result=" + result + " distanceMeters=" + (distanceKM * 1000));
return result;
}
@Override
protected Boolean distanceRangeContainsPoint(double centerLat, double centerLon, double minRadiusMeters, double radiusMeters, double pointLat, double pointLon) {
final double d = SloppyMath.haversin(centerLat, centerLon, pointLat, pointLon)*1000.0;
final double d = GeoDistanceUtils.haversin(centerLat, centerLon, pointLat, pointLon);
return d >= minRadiusMeters && d <= radiusMeters;
}

View File

@ -28,6 +28,7 @@ import org.apache.lucene.index.RandomIndexWriter;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.BaseGeoPointTestCase;
import org.apache.lucene.util.GeoRect;
import org.apache.lucene.util.GeoRelationUtils;
import org.apache.lucene.util.GeoUtils;
import org.apache.lucene.util.SloppyMath;
import org.apache.lucene.util.TestUtil;
@ -171,11 +172,11 @@ public class TestGeoPointQuery extends BaseGeoPointTestCase {
}
if (rect.minLon < rect.maxLon) {
return GeoUtils.bboxContains(pointLon, pointLat, rect.minLon, rect.minLat, rect.maxLon, rect.maxLat);
return GeoRelationUtils.pointInRect(pointLon, pointLat, rect.minLon, rect.minLat, rect.maxLon, rect.maxLat);
} else {
// Rect crosses dateline:
return GeoUtils.bboxContains(pointLon, pointLat, -180.0, rect.minLat, rect.maxLon, rect.maxLat)
|| GeoUtils.bboxContains(pointLon, pointLat, rect.minLon, rect.minLat, 180.0, rect.maxLat);
return GeoRelationUtils.pointInRect(pointLon, pointLat, -180.0, rect.minLat, rect.maxLon, rect.maxLat)
|| GeoRelationUtils.pointInRect(pointLon, pointLat, rect.minLon, rect.minLat, 180.0, rect.maxLat);
}
}
@ -221,7 +222,7 @@ public class TestGeoPointQuery extends BaseGeoPointTestCase {
}
public void testRectCrossesCircle() throws Exception {
assertTrue(GeoUtils.rectCrossesCircle(-180, -90, 180, 0.0, 0.667, 0.0, 88000.0));
assertTrue(GeoRelationUtils.rectCrossesCircle(-180, -90, 180, 0.0, 0.667, 0.0, 88000.0));
}
private TopDocs geoDistanceRangeQuery(double lon, double lat, double minRadius, double maxRadius, int limit)
@ -261,9 +262,9 @@ public class TestGeoPointQuery extends BaseGeoPointTestCase {
double yMax = 1;//5;
// test cell crossing poly
assertTrue(GeoUtils.rectCrossesPoly(xMin, yMin, xMax, yMax, px, py, xMinA, yMinA, xMaxA, yMaxA));
assertFalse(GeoUtils.rectCrossesPoly(-5, 0, 0.000001, 5, px, py, xMin, yMin, xMax, yMax));
assertTrue(GeoUtils.rectWithinPoly(-5, 0, -2, 5, px, py, xMin, yMin, xMax, yMax));
assertTrue(GeoRelationUtils.rectCrossesPoly(xMin, yMin, xMax, yMax, px, py, xMinA, yMinA, xMaxA, yMaxA));
assertFalse(GeoRelationUtils.rectCrossesPoly(-5, 0, 0.000001, 5, px, py, xMin, yMin, xMax, yMax));
assertTrue(GeoRelationUtils.rectWithinPoly(-5, 0, -2, 5, px, py, xMin, yMin, xMax, yMax));
}
public void testBBoxCrossDateline() throws Exception {

View File

@ -312,7 +312,7 @@ public class TestGeoUtils extends LuceneTestCase {
// Leaf cell: brute force check all docs that fall within this cell:
for(int docID=0;docID<docLons.length;docID++) {
if (cell.contains(docLons[docID], docLats[docID])) {
double distanceMeters = SloppyMath.haversin(centerLat, centerLon, docLats[docID], docLons[docID]) * 1000.0;
double distanceMeters = GeoDistanceUtils.haversin(centerLat, centerLon, docLats[docID], docLons[docID]);
if (distanceMeters <= radiusMeters) {
if (VERBOSE) {
log.println(" check doc=" + docID + ": match!");
@ -327,7 +327,7 @@ public class TestGeoUtils extends LuceneTestCase {
}
} else {
if (GeoUtils.rectWithinCircle(cell.minLon, cell.minLat, cell.maxLon, cell.maxLat, centerLon, centerLat, radiusMeters)) {
if (GeoRelationUtils.rectWithinCircle(cell.minLon, cell.minLat, cell.maxLon, cell.maxLat, centerLon, centerLat, radiusMeters)) {
// Query circle fully contains this cell, just addAll:
if (VERBOSE) {
log.println(" circle fully contains cell: now addAll");
@ -341,13 +341,13 @@ public class TestGeoUtils extends LuceneTestCase {
}
}
continue;
} else if (GeoUtils.rectWithin(root.minLon, root.minLat, root.maxLon, root.maxLat,
} else if (GeoRelationUtils.rectWithin(root.minLon, root.minLat, root.maxLon, root.maxLat,
cell.minLon, cell.minLat, cell.maxLon, cell.maxLat)) {
// Fall through below to "recurse"
if (VERBOSE) {
log.println(" cell fully contains circle: keep splitting");
}
} else if (GeoUtils.rectCrossesCircle(cell.minLon, cell.minLat, cell.maxLon, cell.maxLat,
} else if (GeoRelationUtils.rectCrossesCircle(cell.minLon, cell.minLat, cell.maxLon, cell.maxLat,
centerLon, centerLat, radiusMeters)) {
// Fall through below to "recurse"
if (VERBOSE) {
@ -415,15 +415,11 @@ public class TestGeoUtils extends LuceneTestCase {
}
/** Tests consistency of GeoUtils.rectWithinCircle, .rectCrossesCircle, .rectWithin and SloppyMath.haversine distance check */
@AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/LUCENE-6846")
public void testGeoRelations() throws Exception {
int numDocs = atLeast(1000);
// boolean useSmallRanges = random().nextBoolean();
// TODO: the GeoUtils APIs have bugs if you use large distances:
boolean useSmallRanges = true;
boolean useSmallRanges = random().nextBoolean();
if (VERBOSE) {
System.out.println("TEST: " + numDocs + " docs useSmallRanges=" + useSmallRanges);
@ -475,22 +471,29 @@ public class TestGeoUtils extends LuceneTestCase {
if (bbox.maxLon < bbox.minLon) {
// Crosses dateline
log.println(" circle crosses dateline; first right query");
log.println(" circle crosses dateline; first left query");
double unwrappedLon = centerLon;
if (unwrappedLon > bbox.maxLon) {
// unwrap left
unwrappedLon += -360.0D;
}
findMatches(hits, log,
new Cell(null,
-180, bbox.minLat,
bbox.maxLon, bbox.maxLat,
0),
centerLon, centerLat, radiusMeters,
docLons, docLats);
log.println(" circle crosses dateline; now left query");
unwrappedLon, centerLat, radiusMeters, docLons, docLats);
log.println(" circle crosses dateline; now right query");
if (unwrappedLon < bbox.maxLon) {
// unwrap right
unwrappedLon += 360.0D;
}
findMatches(hits, log,
new Cell(null,
bbox.minLon, bbox.minLat,
180, bbox.maxLat,
0),
centerLon, centerLat, radiusMeters,
docLons, docLats);
unwrappedLon, centerLat, radiusMeters, docLons, docLats);
} else {
// Start with the root cell that fully contains the shape:
findMatches(hits, log,
@ -510,15 +513,15 @@ public class TestGeoUtils extends LuceneTestCase {
// Done matching, now verify:
for(int docID=0;docID<numDocs;docID++) {
double distanceMeters = SloppyMath.haversin(centerLat, centerLon, docLats[docID], docLons[docID]) * 1000.0;
double distanceMeters = GeoDistanceUtils.haversin(centerLat, centerLon, docLats[docID], docLons[docID]);
boolean expected = distanceMeters <= radiusMeters;
boolean actual = hits.contains(docID);
if (actual != expected) {
if (actual) {
log.println("doc=" + docID + " matched but should not");
log.println("doc=" + docID + " matched but should not on iteration " + iter);
} else {
log.println("doc=" + docID + " did not match but should");
log.println("doc=" + docID + " did not match but should on iteration " + iter);
}
log.println(" lon=" + docLons[docID] + " lat=" + docLats[docID] + " distanceMeters=" + distanceMeters + " vs radiusMeters=" + radiusMeters);
failCount++;