From bf0e59223d0fdf6de28f2b8a495331222e3232c8 Mon Sep 17 00:00:00 2001 From: Mike McCandless Date: Thu, 31 Mar 2016 11:53:57 -0400 Subject: [PATCH] LUCENE-7150: add Geo3DPoint.newDistance/Box/PolygonQuery --- lucene/CHANGES.txt | 3 + .../apache/lucene/spatial3d/Geo3DPoint.java | 167 +++++++++++++++++- .../lucene/spatial3d/TestGeo3DPoint.java | 20 +-- 3 files changed, 174 insertions(+), 16 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index e86f44d20c1..dd71fa71972 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -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% diff --git a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java index 955a2bc0f36..6b34518a769 100644 --- a/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java +++ b/lucene/spatial3d/src/java/org/apache/lucene/spatial3d/Geo3DPoint.java @@ -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. + *

+ * 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. + *

+ * 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 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. + *

+ * @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); + } + } } diff --git a/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/TestGeo3DPoint.java b/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/TestGeo3DPoint.java index 5c0044f4371..ccaf0937f73 100644 --- a/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/TestGeo3DPoint.java +++ b/lucene/spatial3d/src/test/org/apache/lucene/spatial3d/TestGeo3DPoint.java @@ -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.toString()); + Geo3DPoint point = new Geo3DPoint("point", 44.244272, 7.769736); + assertEquals("Geo3DPoint ", 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()); }