Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accurate GeoDistance Function #4499

Merged
merged 1 commit into from
Dec 27, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/reference/query-dsl/filters/geo-distance-filter.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,9 @@ The following are options allowed on the filter:

`distance_type`::

How to compute the distance. Can either be `arc` (better precision) or
`plane` (faster). Defaults to `arc`.
How to compute the distance. Can either be `arc` (better precision),
`sloppy_arc` (faster but less precise) or `plane` (fastest). Defaults
to `sloppy_arc`.

`optimize_bbox`::

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ By default, the distance unit is `km` but it can also accept: `mi` (miles), `in`

<1> The distances will be computed as miles

There are two distance calculation modes: `arc` (the default) and `plane`. The `arc` calculation is the most accurate one but also the more expensive one in terms of performance. The `plane` is faster but less accurate. Consider using `plane` when your search context is "narrow" and spans smaller geographical areas (like cities or even countries). `plane` may return higher error mergins for searches across very large areas (e.g. cross continent search). The distance calculation type can be set using the `distance_type` parameter:
There are two distance calculation modes: `sloppy_arc` (the default), `arc` (most accurate) and `plane` (fastest). The `arc` calculation is the most accurate one but also the more expensive one in terms of performance. The `sloppy_arc` is faster but less accurate. The `plane` is the fastest but least accurate distance function. Consider using `plane` when your search context is "narrow" and spans smaller geographical areas (like cities or even countries). `plane` may return higher error mergins for searches across very large areas (e.g. cross continent search). The distance calculation type can be set using the `distance_type` parameter:

[source,js]
--------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/search/facets/geo-distance-facet.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ itself.
be `mi`, `miles`, `in`, `inch`, `yd`, `yards`, `kilometers`, `mm`, `millimeters`, `cm`, `centimeters`, `m` or `meters`.

|`distance_type` |How to compute the distance. Can either be `arc`
(better precision) or `plane` (faster). Defaults to `arc`.
(better precision), `sloppy_arc` (faster) or `plane` (fastest). Defaults to `sloppy_arc`.
|=======================================================================

==== Value Options
Expand Down
112 changes: 94 additions & 18 deletions src/main/java/org/elasticsearch/common/geo/GeoDistance.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import org.elasticsearch.ElasticSearchIllegalArgumentException;
import org.elasticsearch.common.unit.DistanceUnit;

import java.util.Locale;

/**
* Geo distance calculation.
*/
Expand All @@ -48,6 +50,7 @@ public FixedSourceDistance fixedSourceDistance(double sourceLatitude, double sou
return new PlaneFixedSourceDistance(sourceLatitude, sourceLongitude, unit);
}
},

/**
* Calculates distance factor.
*/
Expand All @@ -71,12 +74,23 @@ public FixedSourceDistance fixedSourceDistance(double sourceLatitude, double sou
}
},
/**
* Calculates distance as points in a globe.
* Calculates distance as points on a globe.
*/
ARC() {
@Override
public double calculate(double sourceLatitude, double sourceLongitude, double targetLatitude, double targetLongitude, DistanceUnit unit) {
return unit.fromMeters(SloppyMath.haversin(sourceLatitude, sourceLongitude, targetLatitude, targetLongitude) * 1000.0);
double longitudeDifference = targetLongitude - sourceLongitude;
double a = Math.toRadians(90D - sourceLatitude);
double c = Math.toRadians(90D - targetLatitude);
double factor = (Math.cos(a) * Math.cos(c)) + (Math.sin(a) * Math.sin(c) * Math.cos(Math.toRadians(longitudeDifference)));

if (factor < -1D) {
return unit.fromMeters(Math.PI * GeoUtils.EARTH_MEAN_RADIUS);
} else if (factor >= 1D) {
return 0;
} else {
return unit.fromMeters(Math.acos(factor) * GeoUtils.EARTH_MEAN_RADIUS);
}
}

@Override
Expand All @@ -88,8 +102,35 @@ public double normalize(double distance, DistanceUnit unit) {
public FixedSourceDistance fixedSourceDistance(double sourceLatitude, double sourceLongitude, DistanceUnit unit) {
return new ArcFixedSourceDistance(sourceLatitude, sourceLongitude, unit);
}
},
/**
* Calculates distance as points on a globe in a sloppy way. Close to the pole areas the accuracy
* of this function decreases.
*/
SLOPPY_ARC() {

@Override
public double normalize(double distance, DistanceUnit unit) {
return distance;
}

@Override
public double calculate(double sourceLatitude, double sourceLongitude, double targetLatitude, double targetLongitude, DistanceUnit unit) {
return unit.fromMeters(SloppyMath.haversin(sourceLatitude, sourceLongitude, targetLatitude, targetLongitude) * 1000.0);
}

@Override
public FixedSourceDistance fixedSourceDistance(double sourceLatitude, double sourceLongitude, DistanceUnit unit) {
return new SloppyArcFixedSourceDistance(sourceLatitude, sourceLongitude, unit);
}
};

/**
* Default {@link GeoDistance} function. This method should be used, If no specific function has been selected.
* This is an alias for <code>SLOPPY_ARC</code>
*/
public static final GeoDistance DEFAULT = SLOPPY_ARC;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!


public abstract double normalize(double distance, DistanceUnit unit);

public abstract double calculate(double sourceLatitude, double sourceLongitude, double targetLatitude, double targetLongitude, DistanceUnit unit);
Expand Down Expand Up @@ -134,15 +175,31 @@ public static DistanceBoundingCheck distanceBoundingCheck(double sourceLatitude,
return new SimpleDistanceBoundingCheck(topLeft, bottomRight);
}

public static GeoDistance fromString(String s) {
if ("plane".equals(s)) {
/**
* Get a {@link GeoDistance} according to a given name. Valid values are
*
* <ul>
* <li><b>plane</b> for <code>GeoDistance.PLANE</code></li>
* <li><b>sloppy_arc</b> for <code>GeoDistance.SLOPPY_ARC</code></li>
* <li><b>factor</b> for <code>GeoDistance.FACTOR</code></li>
* <li><b>arc</b> for <code>GeoDistance.ARC</code></li>
* </ul>
*
* @param name name of the {@link GeoDistance}
* @return a {@link GeoDistance}
*/
public static GeoDistance fromString(String name) {
name = name.toLowerCase(Locale.ROOT);
if ("plane".equals(name)) {
return PLANE;
} else if ("arc".equals(s)) {
} else if ("arc".equals(name)) {
return ARC;
} else if ("factor".equals(s)) {
} else if ("sloppy_arc".equals(name)) {
return SLOPPY_ARC;
} else if ("factor".equals(name)) {
return FACTOR;
}
throw new ElasticSearchIllegalArgumentException("No geo distance for [" + s + "]");
throw new ElasticSearchIllegalArgumentException("No geo distance for [" + name + "]");
}

public static interface FixedSourceDistance {
Expand Down Expand Up @@ -253,18 +310,14 @@ public double calculate(double targetLatitude, double targetLongitude) {

public static class FactorFixedSourceDistance implements FixedSourceDistance {

private final double sourceLatitude;
private final double sourceLongitude;
private final double earthRadius;

private final double a;
private final double sinA;
private final double cosA;

public FactorFixedSourceDistance(double sourceLatitude, double sourceLongitude, DistanceUnit unit) {
this.sourceLatitude = sourceLatitude;
this.sourceLongitude = sourceLongitude;
this.earthRadius = unit.getEarthRadius();
this.a = Math.toRadians(90D - sourceLatitude);
this.sinA = Math.sin(a);
this.cosA = Math.cos(a);
Expand All @@ -278,21 +331,44 @@ public double calculate(double targetLatitude, double targetLongitude) {
}
}

public static class ArcFixedSourceDistance implements FixedSourceDistance {

private final double sourceLatitude;
private final double sourceLongitude;
private final DistanceUnit unit;
/**
* Basic implementation of {@link FixedSourceDistance}. This class keeps the basic parameters for a distance
* functions based on a fixed source. Namely latitude, longitude and unit.
*/
public static abstract class FixedSourceDistanceBase implements FixedSourceDistance {
protected final double sourceLatitude;
protected final double sourceLongitude;
protected final DistanceUnit unit;

public ArcFixedSourceDistance(double sourceLatitude, double sourceLongitude, DistanceUnit unit) {
public FixedSourceDistanceBase(double sourceLatitude, double sourceLongitude, DistanceUnit unit) {
this.sourceLatitude = sourceLatitude;
this.sourceLongitude = sourceLongitude;
this.unit = unit;
}
}

public static class ArcFixedSourceDistance extends FixedSourceDistanceBase {

public ArcFixedSourceDistance(double sourceLatitude, double sourceLongitude, DistanceUnit unit) {
super(sourceLatitude, sourceLongitude, unit);
}

@Override
public double calculate(double targetLatitude, double targetLongitude) {
return unit.fromMeters(SloppyMath.haversin(sourceLatitude, sourceLongitude, targetLatitude, targetLongitude) * 1000.0);
return ARC.calculate(sourceLatitude, sourceLongitude, targetLatitude, targetLongitude, unit);
}

}

public static class SloppyArcFixedSourceDistance extends FixedSourceDistanceBase {

public SloppyArcFixedSourceDistance(double sourceLatitude, double sourceLongitude, DistanceUnit unit) {
super(sourceLatitude, sourceLongitude, unit);
}

@Override
public double calculate(double targetLatitude, double targetLongitude) {
return SLOPPY_ARC.calculate(sourceLatitude, sourceLongitude, targetLatitude, targetLongitude, unit);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public Filter parse(QueryParseContext parseContext) throws IOException, QueryPar
double distance = 0;
Object vDistance = null;
DistanceUnit unit = DistanceUnit.KILOMETERS; // default unit
GeoDistance geoDistance = GeoDistance.ARC;
GeoDistance geoDistance = GeoDistance.DEFAULT;
String optimizeBbox = "memory";
boolean normalizeLon = true;
boolean normalizeLat = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public Filter parse(QueryParseContext parseContext) throws IOException, QueryPar
boolean includeLower = true;
boolean includeUpper = true;
DistanceUnit unit = DistanceUnit.KILOMETERS; // default unit
GeoDistance geoDistance = GeoDistance.ARC;
GeoDistance geoDistance = GeoDistance.DEFAULT;
String optimizeBbox = "memory";
boolean normalizeLon = true;
boolean normalizeLat = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ static class GeoFieldDataScoreFunction extends AbstractDistanceScoreFunction {
private final IndexGeoPointFieldData<?> fieldData;
private GeoPointValues geoPointValues = null;

private static final GeoDistance distFunction = GeoDistance.fromString("arc");
private static final GeoDistance distFunction = GeoDistance.DEFAULT;

public GeoFieldDataScoreFunction(GeoPoint origin, double scale, double decay, double offset, DecayFunction func,
IndexGeoPointFieldData<?> fieldData) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public AggregatorFactory parse(String aggregationName, XContentParser parser, Se
List<RangeAggregator.Range> ranges = null;
GeoPoint origin = null;
DistanceUnit unit = DistanceUnit.KILOMETERS;
GeoDistance distanceType = GeoDistance.ARC;
GeoDistance distanceType = GeoDistance.DEFAULT;
boolean keyed = false;

XContentParser.Token token;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import com.google.common.collect.Lists;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.geo.GeoDistance;
import org.elasticsearch.common.geo.GeoHashUtils;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.GeoUtils;
import org.elasticsearch.common.inject.Inject;
Expand All @@ -32,7 +31,6 @@
import org.elasticsearch.index.fielddata.IndexGeoPointFieldData;
import org.elasticsearch.index.fielddata.IndexNumericFieldData;
import org.elasticsearch.index.mapper.FieldMapper;
import org.elasticsearch.index.mapper.geo.GeoPointFieldMapper;
import org.elasticsearch.search.facet.FacetExecutor;
import org.elasticsearch.search.facet.FacetParser;
import org.elasticsearch.search.facet.FacetPhaseExecutionException;
Expand Down Expand Up @@ -77,7 +75,7 @@ public FacetExecutor parse(String facetName, XContentParser parser, SearchContex
Map<String, Object> params = null;
GeoPoint point = new GeoPoint();
DistanceUnit unit = DistanceUnit.KILOMETERS;
GeoDistance geoDistance = GeoDistance.ARC;
GeoDistance geoDistance = GeoDistance.DEFAULT;
List<GeoDistanceFacet.Entry> entries = Lists.newArrayList();

boolean normalizeLon = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public SortField parse(XContentParser parser, SearchContext context) throws Exce
String fieldName = null;
GeoPoint point = new GeoPoint();
DistanceUnit unit = DistanceUnit.KILOMETERS;
GeoDistance geoDistance = GeoDistance.ARC;
GeoDistance geoDistance = GeoDistance.DEFAULT;
boolean reverse = false;
SortMode sortMode = null;
String nestedPath = null;
Expand Down
61 changes: 22 additions & 39 deletions src/test/java/org/apache/lucene/util/SloppyMathTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,17 @@
package org.apache.lucene.util;

import org.elasticsearch.common.geo.GeoDistance;
import org.elasticsearch.common.geo.GeoUtils;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.junit.Test;

import static org.hamcrest.number.IsCloseTo.closeTo;

public class SloppyMathTests extends ElasticsearchTestCase {
public class SloppyMathTests extends ElasticsearchTestCase {

@Test
public void testAccuracy() {
for (double lat1 = -90; lat1 <= 90; lat1+=1) {
for (double lat1 = -89; lat1 <= 89; lat1+=1) {
final double lon1 = randomLongitude();

for (double i = -180; i <= 180; i+=1) {
Expand All @@ -45,12 +44,12 @@ public void testAccuracy() {

@Test
public void testSloppyMath() {
assertThat(GeoDistance.ARC.calculate(-46.645, -171.057, -46.644, -171.058, DistanceUnit.METERS), closeTo(134.87709, maxError(134.87709)));
assertThat(GeoDistance.ARC.calculate(-77.912, -81.173, -77.912, -81.171, DistanceUnit.METERS), closeTo(46.57161, maxError(46.57161)));
assertThat(GeoDistance.ARC.calculate(65.75, -20.708, 65.75, -20.709, DistanceUnit.METERS), closeTo(45.66996, maxError(45.66996)));
assertThat(GeoDistance.ARC.calculate(-86.9, 53.738, -86.9, 53.741, DistanceUnit.METERS), closeTo(18.03998, maxError(18.03998)));
assertThat(GeoDistance.ARC.calculate(89.041, 115.93, 89.04, 115.946, DistanceUnit.METERS), closeTo(115.11711, maxError(115.11711)));
assertThat(GeoDistance.SLOPPY_ARC.calculate(-46.645, -171.057, -46.644, -171.058, DistanceUnit.METERS), closeTo(134.87709, maxError(134.87709)));
assertThat(GeoDistance.SLOPPY_ARC.calculate(-77.912, -81.173, -77.912, -81.171, DistanceUnit.METERS), closeTo(46.57161, maxError(46.57161)));
assertThat(GeoDistance.SLOPPY_ARC.calculate(65.75, -20.708, 65.75, -20.709, DistanceUnit.METERS), closeTo(45.66996, maxError(45.66996)));
assertThat(GeoDistance.SLOPPY_ARC.calculate(-86.9, 53.738, -86.9, 53.741, DistanceUnit.METERS), closeTo(18.03998, maxError(18.03998)));
assertThat(GeoDistance.SLOPPY_ARC.calculate(89.041, 115.93, 89.04, 115.946, DistanceUnit.METERS), closeTo(115.11711, maxError(115.11711)));

testSloppyMath(DistanceUnit.METERS, 0.01, 5, 45, 90);
testSloppyMath(DistanceUnit.KILOMETERS, 0.01, 5, 45, 90);
testSloppyMath(DistanceUnit.INCH, 0.01, 5, 45, 90);
Expand All @@ -66,50 +65,34 @@ private void testSloppyMath(DistanceUnit unit, double...deltaDeg) {
final double lon1 = randomLongitude();
logger.info("testing SloppyMath with {} at \"{}, {}\"", unit, lat1, lon1);

GeoDistance.ArcFixedSourceDistance src = new GeoDistance.ArcFixedSourceDistance(lat1, lon1, unit);

for (int test = 0; test < deltaDeg.length; test++) {
for (int i = 0; i < 100; i++) {
// crop pole areas, sine we now there the function
// is not accurate around lat(89°, 90°) and lat(-90°, -89°)
final double lat2 = Math.max(-89.0, Math.min(+89.0, lat1 + (randomDouble() - 0.5) * 2 * deltaDeg[test]));
final double lon2 = lon1 + (randomDouble() - 0.5) * 2 * deltaDeg[test];
final double lat2 = lat1 + (randomDouble() - 0.5) * 2 * deltaDeg[test];

final double accurate = unit.fromMeters(accurateHaversin(lat1, lon1, lat2, lon2));
final double dist1 = GeoDistance.ARC.calculate(lat1, lon1, lat2, lon2, unit);
final double dist2 = src.calculate(lat2, lon2);

final double accurate = GeoDistance.ARC.calculate(lat1, lon1, lat2, lon2, unit);
final double dist = GeoDistance.SLOPPY_ARC.calculate(lat1, lon1, lat2, lon2, unit);

assertThat("distance between("+lat1+", "+lon1+") and ("+lat2+", "+lon2+"))", dist1, closeTo(accurate, maxError(accurate)));
assertThat("distance between("+lat1+", "+lon1+") and ("+lat2+", "+lon2+"))", dist2, closeTo(accurate, maxError(accurate)));
assertThat("distance between("+lat1+", "+lon1+") and ("+lat2+", "+lon2+"))", dist, closeTo(accurate, maxError(accurate)));
}
}
}

// Slow but accurate implementation of the haversin function
private static double accurateHaversin(double lat1, double lon1, double lat2, double lon2) {
double longitudeDifference = lon2 - lon1;
double a = Math.toRadians(90D - lat1);
double c = Math.toRadians(90D - lat2);
double factor = (Math.cos(a) * Math.cos(c)) + (Math.sin(a) * Math.sin(c) * Math.cos(Math.toRadians(longitudeDifference)));

if (factor < -1D) {
return Math.PI * GeoUtils.EARTH_MEAN_RADIUS;
} else if (factor >= 1D) {
return 0;
} else {
return Math.acos(factor) * GeoUtils.EARTH_MEAN_RADIUS;
}
}


private static void assertAccurate(double lat1, double lon1, double lat2, double lon2) {
double accurate = accurateHaversin(lat1, lon1, lat2, lon2);
double sloppy = GeoDistance.ARC.calculate(lat1, lon1, lat2, lon2, DistanceUnit.METERS);
double accurate = GeoDistance.ARC.calculate(lat1, lon1, lat2, lon2, DistanceUnit.METERS);
double sloppy = GeoDistance.SLOPPY_ARC.calculate(lat1, lon1, lat2, lon2, DistanceUnit.METERS);
assertThat("distance between("+lat1+", "+lon1+") and ("+lat2+", "+lon2+"))", sloppy, closeTo(accurate, maxError(accurate)));
}

private static final double randomLatitude() {
return (getRandom().nextDouble() - 0.5) * 180d;
// crop pole areas, sine we now there the function
// is not accurate around lat(89°, 90°) and lat(-90°, -89°)
return (getRandom().nextDouble() - 0.5) * 178.0;
}

private static final double randomLongitude() {
return (getRandom().nextDouble() - 0.5) * 360d;
return (getRandom().nextDouble() - 0.5) * 360.0;
}
}