LUCENE-7150: add Geo3DPoint.newDistance/Box/PolygonQuery

This commit is contained in:
Mike McCandless 2016-03-31 11:53:57 -04:00
parent 0b2040d61c
commit bf0e59223d
3 changed files with 174 additions and 16 deletions

View File

@ -21,6 +21,9 @@ API Changes
* LUCENE-7141: Switch OfflineSorter's ByteSequencesReader to
BytesRefIterator (Mike McCandless)
* LUCENE-7150: Spatial3d gets useful APIs to create common shape
queries, matching LatLonPoint. (Karl Wright via Mike McCandless)
Optimizations
* LUCENE-7071: Reduce bytes copying in OfflineSorter, giving ~10%

View File

@ -16,12 +16,19 @@
*/
package org.apache.lucene.spatial3d;
import java.util.List;
import java.util.ArrayList;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.index.PointValues;
import org.apache.lucene.spatial3d.geom.GeoPoint;
import org.apache.lucene.spatial3d.geom.GeoShape;
import org.apache.lucene.spatial3d.geom.PlanetModel;
import org.apache.lucene.spatial3d.geom.GeoCircleFactory;
import org.apache.lucene.spatial3d.geom.GeoBBoxFactory;
import org.apache.lucene.spatial3d.geom.GeoPolygonFactory;
import org.apache.lucene.spatial3d.geom.GeoPath;
import org.apache.lucene.search.Query;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.NumericUtils;
@ -39,6 +46,12 @@ import org.apache.lucene.util.NumericUtils;
* @lucene.experimental */
public final class Geo3DPoint extends Field {
/** Mean radius of the earth, in meters */
protected final static double MEAN_EARTH_RADIUS_METERS = 6371008.7714;
/** How many radians are in one earth surface meter */
protected final static double RADIANS_PER_METER = 1.0 / MEAN_EARTH_RADIUS_METERS;
/** Indexing {@link FieldType}. */
public static final FieldType TYPE = new FieldType();
static {
@ -47,21 +60,135 @@ public final class Geo3DPoint extends Field {
}
/**
* Creates a new Geo3DPoint field with the specified lat, lon (in radians).
* Creates a new Geo3DPoint field with the specified latitude, longitude (in degrees).
*
* @throws IllegalArgumentException if the field name is null or lat or lon are out of bounds
* @throws IllegalArgumentException if the field name is null or latitude or longitude are out of bounds
*/
public Geo3DPoint(String name, double lat, double lon) {
public Geo3DPoint(String name, double latitude, double longitude) {
super(name, TYPE);
// Translate lat/lon to x,y,z:
final GeoPoint point = new GeoPoint(PlanetModel.WGS84, lat, lon);
checkLatitude(latitude);
checkLongitude(longitude);
// Translate latitude/longitude to x,y,z:
final GeoPoint point = new GeoPoint(PlanetModel.WGS84, fromDegrees(latitude), fromDegrees(longitude));
fillFieldsData(point.x, point.y, point.z);
}
/** Converts degress to radians */
protected static double fromDegrees(final double degrees) {
return Math.toRadians(degrees);
}
/** Converts earth-surface meters to radians */
protected static double fromMeters(final double meters) {
return meters * RADIANS_PER_METER;
}
/**
* Create a query for matching points within the specified distance of the supplied location.
* @param field field name. must not be null. Note that because
* {@link PlanetModel#WGS84} is used, this query is approximate and may have up
* to 0.5% error.
*
* @param latitude latitude at the center: must be within standard +/-90 coordinate bounds.
* @param longitude longitude at the center: must be within standard +/-180 coordinate bounds.
* @param radiusMeters maximum distance from the center in meters: must be non-negative and finite.
* @return query matching points within this distance
* @throws IllegalArgumentException if {@code field} is null, location has invalid coordinates, or radius is invalid.
*/
public static Query newDistanceQuery(final String field, final double latitude, final double longitude, final double radiusMeters) {
checkLatitude(latitude);
checkLongitude(longitude);
final GeoShape shape = GeoCircleFactory.makeGeoCircle(PlanetModel.WGS84, fromDegrees(latitude), fromDegrees(longitude), fromMeters(radiusMeters));
return newShapeQuery(field, shape);
}
/**
* Create a query for matching a box.
* <p>
* The box may cross over the dateline.
* @param field field name. must not be null.
* @param minLatitude latitude lower bound: must be within standard +/-90 coordinate bounds.
* @param maxLatitude latitude upper bound: must be within standard +/-90 coordinate bounds.
* @param minLongitude longitude lower bound: must be within standard +/-180 coordinate bounds.
* @param maxLongitude longitude upper bound: must be within standard +/-180 coordinate bounds.
* @return query matching points within this box
* @throws IllegalArgumentException if {@code field} is null, or the box has invalid coordinates.
*/
public static Query newBoxQuery(final String field, final double minLatitude, final double maxLatitude, final double minLongitude, final double maxLongitude) {
checkLatitude(minLatitude);
checkLongitude(minLongitude);
checkLatitude(maxLatitude);
checkLongitude(maxLongitude);
final GeoShape shape = GeoBBoxFactory.makeGeoBBox(PlanetModel.WGS84,
fromDegrees(maxLatitude), fromDegrees(minLatitude), fromDegrees(minLongitude), fromDegrees(maxLongitude));
return newShapeQuery(field, shape);
}
/**
* Create a query for matching a polygon.
* <p>
* The supplied {@code polyLatitudes}/{@code polyLongitudes} must be clockwise or counter-clockwise.
* @param field field name. must not be null.
* @param polyLatitudes latitude values for points of the polygon: must be within standard +/-90 coordinate bounds.
* @param polyLongitudes longitude values for points of the polygon: must be within standard +/-180 coordinate bounds.
* @return query matching points within this polygon
*/
public static Query newPolygonQuery(final String field, final double[] polyLatitudes, final double[] polyLongitudes) {
if (polyLatitudes.length != polyLongitudes.length) {
throw new IllegalArgumentException("same number of latitudes and longitudes required");
}
if (polyLatitudes.length < 4) {
throw new IllegalArgumentException("need three or more points");
}
if (polyLatitudes[0] != polyLatitudes[polyLatitudes.length-1] || polyLongitudes[0] != polyLongitudes[polyLongitudes.length-1]) {
throw new IllegalArgumentException("last point must equal first point");
}
final List<GeoPoint> polyPoints = new ArrayList<>(polyLatitudes.length-1);
for (int i = 0; i < polyLatitudes.length-1; i++) {
final double latitude = polyLatitudes[i];
final double longitude = polyLongitudes[i];
checkLatitude(latitude);
checkLongitude(longitude);
polyPoints.add(new GeoPoint(PlanetModel.WGS84, fromDegrees(latitude), fromDegrees(longitude)));
}
// We don't know what the sense of the polygon is without providing the index of one vertex we know to be convex.
// Since that doesn't fit with the "super-simple API" requirements, we just use the index of the first one, and people have to just
// know to do it that way.
final int convexPointIndex = 0;
final GeoShape shape = GeoPolygonFactory.makeGeoPolygon(PlanetModel.WGS84, polyPoints, convexPointIndex);
return newShapeQuery(field, shape);
}
/**
* Create a query for matching a path.
* <p>
* @param field field name. must not be null.
* @param pathLatitudes latitude values for points of the path: must be within standard +/-90 coordinate bounds.
* @param pathLongitudes longitude values for points of the path: must be within standard +/-180 coordinate bounds.
* @param pathWidthMeters width of the path in meters.
* @return query matching points within this polygon
*/
public static Query newPathQuery(final String field, final double[] pathLatitudes, final double[] pathLongitudes, final double pathWidthMeters) {
if (pathLatitudes.length != pathLongitudes.length) {
throw new IllegalArgumentException("same number of latitudes and longitudes required");
}
final GeoPoint[] points = new GeoPoint[pathLatitudes.length];
for (int i = 0; i < pathLatitudes.length; i++) {
final double latitude = pathLatitudes[i];
final double longitude = pathLongitudes[i];
checkLatitude(latitude);
checkLongitude(longitude);
points[i] = new GeoPoint(PlanetModel.WGS84, fromDegrees(latitude), fromDegrees(longitude));
}
final GeoShape shape = new GeoPath(PlanetModel.WGS84, fromMeters(pathWidthMeters), points);
return newShapeQuery(field, shape);
}
/**
* Creates a new Geo3DPoint field with the specified x,y,z.
*
* @throws IllegalArgumentException if the field name is null or lat or lon are out of bounds
* @throws IllegalArgumentException if the field name is null or latitude or longitude are out of bounds
*/
public Geo3DPoint(String name, double x, double y, double z) {
super(name, TYPE);
@ -112,4 +239,32 @@ public final class Geo3DPoint extends Field {
result.append('>');
return result.toString();
}
// TODO LUCENE-7152: share this with GeoUtils.java from spatial module
/** Minimum longitude value. */
private static final double MIN_LON_INCL = -180.0D;
/** Maximum longitude value. */
private static final double MAX_LON_INCL = 180.0D;
/** Minimum latitude value. */
private static final double MIN_LAT_INCL = -90.0D;
/** Maximum latitude value. */
private static final double MAX_LAT_INCL = 90.0D;
/** validates latitude value is within standard +/-90 coordinate bounds */
private static void checkLatitude(double latitude) {
if (Double.isNaN(latitude) || latitude < MIN_LAT_INCL || latitude > MAX_LAT_INCL) {
throw new IllegalArgumentException("invalid latitude " + latitude + "; must be between " + MIN_LAT_INCL + " and " + MAX_LAT_INCL);
}
}
/** validates longitude value is within standard +/-180 coordinate bounds */
private static void checkLongitude(double longitude) {
if (Double.isNaN(longitude) || longitude < MIN_LON_INCL || longitude > MAX_LON_INCL) {
throw new IllegalArgumentException("invalid longitude " + longitude + "; must be between " + MIN_LON_INCL + " and " + MAX_LON_INCL);
}
}
}

View File

@ -115,7 +115,7 @@ public class TestGeo3DPoint extends LuceneTestCase {
iwc.setCodec(getCodec());
IndexWriter w = new IndexWriter(dir, iwc);
Document doc = new Document();
doc.add(new Geo3DPoint("field", toRadians(50.7345267), toRadians(-97.5303555)));
doc.add(new Geo3DPoint("field", 50.7345267, -97.5303555));
w.addDocument(doc);
IndexReader r = DirectoryReader.open(w);
// We can't wrap with "exotic" readers because the query must see the BKD3DDVFormat:
@ -128,7 +128,7 @@ public class TestGeo3DPoint extends LuceneTestCase {
}
private static double toRadians(double degrees) {
return Math.PI*(degrees/360.0);
return Math.PI*(degrees/180.0);
}
private static PlanetModel getPlanetModel() {
@ -508,13 +508,13 @@ public class TestGeo3DPoint extends LuceneTestCase {
if (x == 0) {
// Identical lat to old point
lats[docID] = lats[oldDocID];
lons[docID] = toRadians(randomLon());
lons[docID] = randomLon();
if (VERBOSE) {
System.err.println(" doc=" + docID + " lat=" + lats[docID] + " lon=" + lons[docID] + " (same lat as doc=" + oldDocID + ")");
}
} else if (x == 1) {
// Identical lon to old point
lats[docID] = toRadians(randomLat());
lats[docID] = randomLat();
lons[docID] = lons[oldDocID];
if (VERBOSE) {
System.err.println(" doc=" + docID + " lat=" + lats[docID] + " lon=" + lons[docID] + " (same lon as doc=" + oldDocID + ")");
@ -529,8 +529,8 @@ public class TestGeo3DPoint extends LuceneTestCase {
}
}
} else {
lats[docID] = toRadians(randomLat());
lons[docID] = toRadians(randomLon());
lats[docID] = randomLat();
lons[docID] = randomLon();
haveRealDoc = true;
if (VERBOSE) {
System.err.println(" doc=" + docID + " lat=" + lats[docID] + " lon=" + lons[docID]);
@ -759,7 +759,7 @@ public class TestGeo3DPoint extends LuceneTestCase {
if (Double.isNaN(lats[id]) == false) {
// Accurate point:
GeoPoint point1 = new GeoPoint(PlanetModel.WGS84, lats[id], lons[id]);
GeoPoint point1 = new GeoPoint(PlanetModel.WGS84, toRadians(lats[id]), toRadians(lons[id]));
// Quantized point (32 bits per dim):
GeoPoint point2 = quantize(PlanetModel.WGS84.getMaximumMagnitude(), point1);
@ -795,12 +795,12 @@ public class TestGeo3DPoint extends LuceneTestCase {
}
public void testToString() {
Geo3DPoint point = new Geo3DPoint("point", toRadians(44.244272), toRadians(7.769736));
assertEquals("Geo3DPoint <point: x=0.9248467864160119 y=0.06280434265368656 z=0.37682349005486243>", point.toString());
Geo3DPoint point = new Geo3DPoint("point", 44.244272, 7.769736);
assertEquals("Geo3DPoint <point: x=0.709426287693908 y=0.09679758561541502 z=0.6973564369288621>", point.toString());
}
public void testShapeQueryToString() {
assertEquals("PointInGeo3DShapeQuery: field=point: Shape: GeoStandardCircle: {planetmodel=PlanetModel.WGS84, center=[lat=0.3861041107739683, lon=0.06780373760536706], radius=0.1(5.729577951308232)}",
assertEquals("PointInGeo3DShapeQuery: field=point: Shape: GeoStandardCircle: {planetmodel=PlanetModel.WGS84, center=[lat=0.7722082215479366, lon=0.13560747521073413], radius=0.1(5.729577951308232)}",
Geo3DPoint.newShapeQuery("point", GeoCircleFactory.makeGeoCircle(PlanetModel.WGS84, toRadians(44.244272), toRadians(7.769736), 0.1)).toString());
}