blob: 5f64dbb935934e74e4722e0274821bf11cc0a719 [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.spatial.util;
import org.apache.lucene.util.SloppyMath;
/**
* Reusable geo-spatial distance utility methods.
*
* @lucene.experimental
*/
public class GeoDistanceUtils {
/** error threshold for point-distance queries (in percent) NOTE: Guideline from USGS is 0.005 **/
public static final double DISTANCE_PCT_ERR = 0.005;
// No instance:
private 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 = SloppyMath.TO_RADIANS * (lat2 - lat1);
double dLon = SloppyMath.TO_RADIANS * (lon2 - lon1);
lat1 = SloppyMath.TO_RADIANS * (lat1);
lat2 = SloppyMath.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
* 22m better accuracy (in worst case) than haversine
*
* @param lonA longitudinal coordinate of point A (in degrees)
* @param latA latitudinal coordinate of point A (in degrees)
* @param lonB longitudinal coordinate of point B (in degrees)
* @param latB latitudinal coordinate of point B (in degrees)
* @return distance (in meters) between point A and point B
*/
public static final double vincentyDistance(final double lonA, final double latA, final double lonB, final double latB) {
final double L = StrictMath.toRadians(lonB - lonA);
final double oF = 1 - GeoProjectionUtils.FLATTENING;
final double U1 = StrictMath.atan(oF * StrictMath.tan(StrictMath.toRadians(latA)));
final double U2 = StrictMath.atan(oF * StrictMath.tan(StrictMath.toRadians(latB)));
final double sU1 = StrictMath.sin(U1);
final double cU1 = StrictMath.cos(U1);
final double sU2 = StrictMath.sin(U2);
final double cU2 = StrictMath.cos(U2);
double sigma, sinSigma, cosSigma;
double sinAlpha, cos2Alpha, cos2SigmaM;
double lambda = L;
double lambdaP;
double iters = 100;
double sinLambda, cosLambda, c;
do {
sinLambda = StrictMath.sin(lambda);
cosLambda = Math.cos(lambda);
sinSigma = Math.sqrt((cU2 * sinLambda) * (cU2 * sinLambda) + (cU1 * sU2 - sU1 * cU2 * cosLambda)
* (cU1 * sU2 - sU1 * cU2 * cosLambda));
if (sinSigma == 0) {
return 0;
}
cosSigma = sU1 * sU2 + cU1 * cU2 * cosLambda;
sigma = Math.atan2(sinSigma, cosSigma);
sinAlpha = cU1 * cU2 * sinLambda / sinSigma;
cos2Alpha = 1 - sinAlpha * sinAlpha;
cos2SigmaM = cosSigma - 2 * sU1 * sU2 / cos2Alpha;
c = GeoProjectionUtils.FLATTENING/16 * cos2Alpha * (4 + GeoProjectionUtils.FLATTENING * (4 - 3 * cos2Alpha));
lambdaP = lambda;
lambda = L + (1 - c) * GeoProjectionUtils.FLATTENING * sinAlpha * (sigma + c * sinSigma * (cos2SigmaM + c * cosSigma *
(-1 + 2 * cos2SigmaM * cos2SigmaM)));
} while (StrictMath.abs(lambda - lambdaP) > 1E-12 && --iters > 0);
if (iters == 0) {
return 0;
}
final double uSq = cos2Alpha * (GeoProjectionUtils.SEMIMAJOR_AXIS2 - GeoProjectionUtils.SEMIMINOR_AXIS2) / (GeoProjectionUtils.SEMIMINOR_AXIS2);
final double A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)));
final double B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)));
final double deltaSigma = B * sinSigma * (cos2SigmaM + B/4 * (cosSigma * (-1 + 2 * cos2SigmaM * cos2SigmaM) - B/6 * cos2SigmaM
* (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM * cos2SigmaM)));
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
* @param distance distance in meters to convert to degrees lon
* @return Sloppy distance in degrees longitude for provided distance in meters
*/
public static double distanceToDegreesLon(double lat, double distance) {
distance /= 1000.0;
// convert latitude to radians
lat = StrictMath.toRadians(lat);
// get the diameter at the latitude
final double diameter = SloppyMath.earthDiameter(StrictMath.toRadians(lat));
// compute inverse haversine
double a = StrictMath.sin(distance/diameter);
double h = StrictMath.min(1, a);
h *= h;
double cLat = StrictMath.cos(lat);
return StrictMath.toDegrees(StrictMath.acos(1-((2d*h)/(cLat*cLat))));
}
/**
* Finds the closest point within a rectangle (defined by rMinX, rMinY, rMaxX, rMaxY) to the given (lon, lat) point
* the result is provided in closestPt. When the point is outside the rectangle, the closest point is on an edge
* or corner of the rectangle; else, the closest point is the point itself.
*/
public static void closestPointOnBBox(final double rMinX, final double rMinY, final double rMaxX, final double rMaxY,
final double lon, final double lat, double[] closestPt) {
assert closestPt != null && closestPt.length == 2;
closestPt[0] = 0;
closestPt[1] = 0;
boolean xSet = true;
boolean ySet = true;
if (lon > rMaxX) {
closestPt[0] = rMaxX;
} else if (lon < rMinX) {
closestPt[0] = rMinX;
} else {
xSet = false;
}
if (lat > rMaxY) {
closestPt[1] = rMaxY;
} else if (lat < rMinY) {
closestPt[1] = rMinY;
} else {
ySet = false;
}
if (closestPt[0] == 0 && xSet == false) {
closestPt[0] = lon;
}
if (closestPt[1] == 0 && ySet == false) {
closestPt[1] = lat;
}
}
/** 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 GeoDistanceUtils.haversin(centerLat, centerLon, 0, centerLon);
}
return GeoDistanceUtils.haversin(centerLat, centerLon, centerLat, (GeoUtils.MAX_LON_INCL + centerLon) % 360);
}
/**
* Compute the inverse haversine to determine distance in degrees longitude for provided distance in meters
* @param lat latitude to compute delta degrees lon
* @param distance distance in meters to convert to degrees lon
* @return Sloppy distance in degrees longitude for provided distance in meters
*/
public static double distanceToDegreesLat(double lat, double distance) {
// get the diameter at the latitude
final double diameter = SloppyMath.earthDiameter(StrictMath.toRadians(lat));
distance /= 1000.0;
// compute inverse haversine
double a = StrictMath.sin(distance/diameter);
double h = StrictMath.min(1, a);
h *= h;
return StrictMath.toDegrees(StrictMath.acos(1-(2d*h)));
}
}