LUCENE-8707: Add LatLonShape and XYShape distance query (#587)

This commit is contained in:
Ignacio Vera 2020-02-19 16:03:30 +01:00 committed by GitHub
parent 001a35cc06
commit d48bafb299
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1224 additions and 4 deletions

View File

@ -117,7 +117,9 @@ API Changes
New Features
---------------------
* LUCENE-8903: Add LatLonShape point query. (Ignacio Vera)
* LUCENE-8903: Add LatLonShape and XYShape point query. (Ignacio Vera)
* LUCENE-8707: Add LatLonShape and XYShape distance query. (Ignacio Vera)
Improvements
---------------------

View File

@ -21,6 +21,7 @@ import java.util.List;
import org.apache.lucene.document.ShapeField.QueryRelation; // javadoc
import org.apache.lucene.document.ShapeField.Triangle;
import org.apache.lucene.geo.Circle;
import org.apache.lucene.geo.GeoUtils;
import org.apache.lucene.geo.LatLonGeometry;
import org.apache.lucene.geo.Line;
@ -131,6 +132,11 @@ public class LatLonShape {
return newGeometryQuery(field, queryRelation, pointArray);
}
/** create a query to find all polygons that intersect a provided circle. */
public static Query newDistanceQuery(String field, QueryRelation queryRelation, Circle... circle) {
return newGeometryQuery(field, queryRelation, circle);
}
/** create a query to find all indexed geo shapes that intersect a provided geometry (or array of geometries).
**/
public static Query newGeometryQuery(String field, QueryRelation queryRelation, LatLonGeometry... latLonGeometries) {
@ -143,4 +149,5 @@ public class LatLonShape {
}
return new LatLonShapeQuery(field, queryRelation, latLonGeometries);
}
}

View File

@ -22,6 +22,7 @@ import java.util.List;
import org.apache.lucene.document.ShapeField.QueryRelation; // javadoc
import org.apache.lucene.document.ShapeField.Triangle;
import org.apache.lucene.geo.Tessellator;
import org.apache.lucene.geo.XYCircle;
import org.apache.lucene.geo.XYGeometry;
import org.apache.lucene.geo.XYPoint;
import org.apache.lucene.geo.XYRectangle;
@ -117,6 +118,11 @@ public class XYShape {
return newGeometryQuery(field, queryRelation, pointArray);
}
/** create a query to find all cartesian shapes that intersect a provided circle (or arrays of circles) **/
public static Query newDistanceQuery(String field, QueryRelation queryRelation, XYCircle... circle) {
return newGeometryQuery(field, queryRelation, circle);
}
/** create a query to find all indexed geo shapes that intersect a provided geometry collection
* note: Components do not support dateline crossing
**/

View File

@ -0,0 +1,104 @@
/*
* 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.geo;
/**
* Represents a circle on the earth's surface.
* <p>
* NOTES:
* <ol>
* <li> Latitude/longitude values must be in decimal degrees.
* <li> Radius must be in meters.
* <li>For more advanced GeoSpatial indexing and query operations see the {@code spatial-extras} module
* </ol>
* @lucene.experimental
*/
public final class Circle extends LatLonGeometry {
/** Center latitude */
private final double lat;
/** Center longitude */
private final double lon;
/** radius in meters */
private final double radiusMeters;
/** Max radius allowed, half of the earth mean radius.*/
public static double MAX_RADIUS = GeoUtils.EARTH_MEAN_RADIUS_METERS / 2.0;
/**
* Creates a new circle from the supplied latitude/longitude center and a radius in meters..
*/
public Circle(double lat, double lon, double radiusMeters) {
GeoUtils.checkLatitude(lat);
GeoUtils.checkLongitude(lon);
if (radiusMeters <= 0) {
throw new IllegalArgumentException("radius must be bigger than 0, got " + radiusMeters);
}
if (radiusMeters < MAX_RADIUS == false) {
throw new IllegalArgumentException("radius must be lower than " + MAX_RADIUS + ", got " + radiusMeters);
}
this.lat = lat;
this.lon = lon;
this.radiusMeters = radiusMeters;
}
/** Returns the center's latitude */
public double getLat() {
return lat;
}
/** Returns the center's longitude */
public double getLon() {
return lon;
}
/** Returns the radius in meters */
public double getRadius() {
return radiusMeters;
}
@Override
protected Component2D toComponent2D() {
return Circle2D.create(this);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Circle)) return false;
Circle circle = (Circle) o;
return lat == circle.lat && lon == circle.lon && radiusMeters == circle.radiusMeters;
}
@Override
public int hashCode() {
int result = Double.hashCode(lat);
result = 31 * result + Double.hashCode(lon);
result = 31 * result + Double.hashCode(radiusMeters);
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("CIRCLE(");
sb.append("[" + lat + "," + lon + "]");
sb.append(" radius = " + radiusMeters + " meters");
sb.append(')');
return sb.toString();
}
}

View File

@ -0,0 +1,463 @@
/*
* 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.geo;
import org.apache.lucene.index.PointValues.Relation;
import org.apache.lucene.util.SloppyMath;
/**
* 2D circle implementation containing spatial logic.
*/
class Circle2D implements Component2D {
private final DistanceCalculator calculator;
private Circle2D(DistanceCalculator calculator) {
this.calculator = calculator;
}
@Override
public double getMinX() {
return calculator.getMinX();
}
@Override
public double getMaxX() {
return calculator.getMaxX();
}
@Override
public double getMinY() {
return calculator.getMinY();
}
@Override
public double getMaxY() {
return calculator.getMaxY();
}
@Override
public boolean contains(double x, double y) {
return calculator.contains(x, y);
}
@Override
public Relation relate(double minX, double maxX, double minY, double maxY) {
if (calculator.disjoint(minX, maxX, minY, maxY)) {
return Relation.CELL_OUTSIDE_QUERY;
}
if (calculator.within(minX, maxX, minY, maxY)) {
return Relation.CELL_CROSSES_QUERY;
}
return calculator.relate(minX, maxX, minY, maxY);
}
@Override
public Relation relateTriangle(double minX, double maxX, double minY, double maxY,
double ax, double ay, double bx, double by, double cx, double cy) {
if (calculator.disjoint(minX, maxX, minY, maxY)) {
return Relation.CELL_OUTSIDE_QUERY;
}
if (ax == bx && bx == cx && ay == by && by == cy) {
// indexed "triangle" is a point: shortcut by checking contains
return contains(ax, ay) ? Relation.CELL_INSIDE_QUERY : Relation.CELL_OUTSIDE_QUERY;
} else if (ax == cx && ay == cy) {
// indexed "triangle" is a line segment: shortcut by calling appropriate method
return relateIndexedLineSegment(ax, ay, bx, by);
} else if (ax == bx && ay == by) {
// indexed "triangle" is a line segment: shortcut by calling appropriate method
return relateIndexedLineSegment(bx, by, cx, cy);
} else if (bx == cx && by == cy) {
// indexed "triangle" is a line segment: shortcut by calling appropriate method
return relateIndexedLineSegment(cx, cy, ax, ay);
}
// indexed "triangle" is a triangle:
return relateIndexedTriangle(minX, maxX, minY, maxY, ax, ay, bx, by, cx, cy);
}
@Override
public WithinRelation withinTriangle(double minX, double maxX, double minY, double maxY,
double ax, double ay, boolean ab, double bx, double by, boolean bc, double cx, double cy, boolean ca) {
// short cut, lines and points cannot contain this type of shape
if ((ax == bx && ay == by) || (ax == cx && ay == cy) || (bx == cx && by == cy)) {
return WithinRelation.DISJOINT;
}
if (calculator.disjoint(minX, maxX, minY, maxY)) {
return WithinRelation.DISJOINT;
}
// if any of the points is inside the polygon, the polygon cannot be within this indexed
// shape because points belong to the original indexed shape.
if (contains(ax, ay) || contains(bx, by) || contains(cx, cy)) {
return WithinRelation.NOTWITHIN;
}
WithinRelation relation = WithinRelation.DISJOINT;
// if any of the edges intersects an the edge belongs to the shape then it cannot be within.
// if it only intersects edges that do not belong to the shape, then it is a candidate
// we skip edges at the dateline to support shapes crossing it
if (intersectsLine(ax, ay, bx, by)) {
if (ab == true) {
return WithinRelation.NOTWITHIN;
} else {
relation = WithinRelation.CANDIDATE;
}
}
if (intersectsLine(bx, by, cx, cy)) {
if (bc == true) {
return WithinRelation.NOTWITHIN;
} else {
relation = WithinRelation.CANDIDATE;
}
}
if (intersectsLine(cx, cy, ax, ay)) {
if (ca == true) {
return WithinRelation.NOTWITHIN;
} else {
relation = WithinRelation.CANDIDATE;
}
}
// if any of the edges crosses and edge that does not belong to the shape
// then it is a candidate for within
if (relation == WithinRelation.CANDIDATE) {
return WithinRelation.CANDIDATE;
}
// Check if shape is within the triangle
if (Component2D.pointInTriangle(minX, maxX, minY, maxY, calculator.geX(), calculator.getY(), ax, ay, bx, by, cx, cy) == true) {
return WithinRelation.CANDIDATE;
}
return relation;
}
/** relates an indexed line segment (a "flat triangle") with the polygon */
private Relation relateIndexedLineSegment(double a2x, double a2y, double b2x, double b2y) {
// check endpoints of the line segment
int numCorners = 0;
if (contains(a2x, a2y)) {
++numCorners;
}
if (contains(b2x, b2y)) {
++numCorners;
}
if (numCorners == 2) {
return Relation.CELL_INSIDE_QUERY;
} else if (numCorners == 0) {
if (intersectsLine(a2x, a2y, b2x, b2y)) {
return Relation.CELL_CROSSES_QUERY;
}
return Relation.CELL_OUTSIDE_QUERY;
}
return Relation.CELL_CROSSES_QUERY;
}
/** relates an indexed triangle with the polygon */
private Relation relateIndexedTriangle(double minX, double maxX, double minY, double maxY,
double ax, double ay, double bx, double by, double cx, double cy) {
// check each corner: if < 3 && > 0 are present, its cheaper than crossesSlowly
int numCorners = numberOfTriangleCorners(ax, ay, bx, by, cx, cy);
if (numCorners == 3) {
return Relation.CELL_INSIDE_QUERY;
} else if (numCorners == 0) {
if (Component2D.pointInTriangle(minX, maxX, minY, maxY, calculator.geX(), calculator.getY(), ax, ay, bx, by, cx, cy) == true) {
return Relation.CELL_CROSSES_QUERY;
}
if (intersectsLine(ax, ay, bx, by) ||
intersectsLine(bx, by, cx, cy) ||
intersectsLine(cx, cy, ax, ay)) {
return Relation.CELL_CROSSES_QUERY;
}
return Relation.CELL_OUTSIDE_QUERY;
}
return Relation.CELL_CROSSES_QUERY;
}
private int numberOfTriangleCorners(double ax, double ay, double bx, double by, double cx, double cy) {
int containsCount = 0;
if (contains(ax, ay)) {
containsCount++;
}
if (contains(bx, by)) {
containsCount++;
}
if (containsCount == 1) {
// if one point is inside and the other outside, we know
// already that the triangle intersect.
return containsCount;
}
if (contains(cx, cy)) {
containsCount++;
}
return containsCount;
}
// This methods in a new helper class XYUtil?
private boolean intersectsLine(double aX, double aY, double bX, double bY) {
//Algorithm based on this thread : https://stackoverflow.com/questions/3120357/get-closest-point-to-a-line
final double[] vectorAP = new double[] {calculator.geX() - aX, calculator.getY() - aY};
final double[] vectorAB = new double[] {bX - aX, bY - aY};
final double magnitudeAB = vectorAB[0] * vectorAB[0] + vectorAB[1] * vectorAB[1];
final double dotProduct = vectorAP[0] * vectorAB[0] + vectorAP[1] * vectorAB[1];
final double distance = dotProduct / magnitudeAB;
if (distance < 0 || distance > dotProduct) {
return false;
}
final double pX = aX + vectorAB[0] * distance;
final double pY = aY + vectorAB[1] * distance;
final double minX = StrictMath.min(aX, bX);
final double minY = StrictMath.min(aY, bY);
final double maxX = StrictMath.max(aX, bX);
final double maxY = StrictMath.max(aY, bY);
if (pX >= minX && pX <= maxX && pY >= minY && pY <= maxY) {
return contains(pX, pY);
}
return false;
}
private interface DistanceCalculator {
Relation relate(double minX, double maxX, double minY, double maxY);
boolean contains(double x, double y);
boolean disjoint(double minX, double maxX, double minY, double maxY);
boolean within(double minX, double maxX, double minY, double maxY);
double getMinX();
double getMaxX();
double getMinY();
double getMaxY();
double geX();
double getY();
}
private static class CartesianDistance implements DistanceCalculator {
private final double centerX;
private final double centerY;
private final double radiusSquared;
private final double minX;
private final double maxX;
private final double minY;
private final double maxY;
public CartesianDistance(double centerX, double centerY, double radius) {
this.centerX = centerX;
this.centerY = centerY;
this.minX = Math.max(-Float.MAX_VALUE, centerX - radius);
this.maxX = Math.min(Float.MAX_VALUE, centerX + radius);
this.minY = Math.max(-Float.MAX_VALUE, centerY - radius);
this.maxY = Math.min(Float.MAX_VALUE, centerY + radius);
this.radiusSquared = radius * radius;
}
@Override
public Relation relate(double minX, double maxX, double minY, double maxY) {
if (Component2D.containsPoint(centerX, centerY, minX, maxX, minY, maxY)) {
if (contains(minX, minY) && contains(maxX, minY) && contains(maxX, maxY) && contains(minX, maxY)) {
// we are fully enclosed, collect everything within this subtree
return Relation.CELL_INSIDE_QUERY;
}
} else {
// circle not fully inside, compute closest distance
double sumOfSquaredDiffs = 0.0d;
if (centerX < minX) {
double diff = minX - centerX;
sumOfSquaredDiffs += diff * diff;
} else if (centerX > maxX) {
double diff = maxX - centerX;
sumOfSquaredDiffs += diff * diff;
}
if (centerY < minY) {
double diff = minY - centerY;
sumOfSquaredDiffs += diff * diff;
} else if (centerY > maxY) {
double diff = maxY - centerY;
sumOfSquaredDiffs += diff * diff;
}
if (sumOfSquaredDiffs > radiusSquared) {
// disjoint
return Relation.CELL_OUTSIDE_QUERY;
}
}
return Relation.CELL_CROSSES_QUERY;
}
@Override
public boolean contains(double x, double y) {
final double diffX = x - this.centerX;
final double diffY = y - this.centerY;
return diffX * diffX + diffY * diffY <= radiusSquared;
}
@Override
public boolean disjoint(double minX, double maxX, double minY, double maxY) {
return Component2D.disjoint(this.minX, this.maxX, this.minY, this.maxY, minX, maxX, minY, maxY);
}
@Override
public boolean within(double minX, double maxX, double minY, double maxY) {
return Component2D.within(this.minX, this.maxX, this.minY, this.maxY, minX, maxX, minY, maxY);
}
@Override
public double getMinX() {
return minX;
}
@Override
public double getMaxX() {
return maxX;
}
@Override
public double getMinY() {
return minY;
}
@Override
public double getMaxY() {
return maxY;
}
@Override
public double geX() {
return centerX;
}
@Override
public double getY() {
return centerY;
}
}
private static class HaversinDistance implements DistanceCalculator {
final double centerLat;
final double centerLon;
final double sortKey;
final double axisLat;
final Rectangle rectangle;
final boolean crossesDateline;
public HaversinDistance(double centerLon, double centerLat, double radius) {
this.centerLat = centerLat;
this.centerLon = centerLon;
this.sortKey = GeoUtils.distanceQuerySortKey(radius);
this.axisLat = Rectangle.axisLat(centerLat, radius);
this.rectangle = Rectangle.fromPointDistance(centerLat, centerLon, radius);
this.crossesDateline = rectangle.minLon > rectangle.maxLon;
}
@Override
public Relation relate(double minX, double maxX, double minY, double maxY) {
return GeoUtils.relate(minY, maxY, minX, maxX, centerLat, centerLon, sortKey, axisLat);
}
@Override
public boolean contains(double x, double y) {
return SloppyMath.haversinSortKey(y, x, this.centerLat, this.centerLon) <= sortKey;
}
@Override
public boolean disjoint(double minX, double maxX, double minY, double maxY) {
if (crossesDateline) {
return Component2D.disjoint(rectangle.minLon, GeoUtils.MAX_LON_INCL, rectangle.minLat, rectangle.maxLat, minX, maxX, minY, maxY)
&& Component2D.disjoint(GeoUtils.MIN_LON_INCL, rectangle.maxLon, rectangle.minLat, rectangle.maxLat, minX, maxX, minY, maxY);
} else {
return Component2D.disjoint(rectangle.minLon, rectangle.maxLon, rectangle.minLat, rectangle.maxLat, minX, maxX, minY, maxY);
}
}
@Override
public boolean within(double minX, double maxX, double minY, double maxY) {
if (crossesDateline) {
return Component2D.within(rectangle.minLon, GeoUtils.MAX_LON_INCL, rectangle.minLat, rectangle.maxLat, minX, maxX, minY, maxY)
|| Component2D.within(GeoUtils.MIN_LON_INCL, rectangle.maxLon, rectangle.minLat, rectangle.maxLat, minX, maxX, minY, maxY);
} else {
return Component2D.within(rectangle.minLon, rectangle.maxLon, rectangle.minLat, rectangle.maxLat, minX, maxX, minY, maxY);
}
}
@Override
public double getMinX() {
if (crossesDateline) {
// Component2D does not support boxes that crosses the dateline
return GeoUtils.MIN_LON_INCL;
}
return rectangle.minLon;
}
@Override
public double getMaxX() {
if (crossesDateline) {
// Component2D does not support boxes that crosses the dateline
return GeoUtils.MAX_LON_INCL;
}
return rectangle.maxLon;
}
@Override
public double getMinY() {
return rectangle.minLat;
}
@Override
public double getMaxY() {
return rectangle.maxLat;
}
@Override
public double geX() {
return centerLon;
}
@Override
public double getY() {
return centerLat;
}
}
/** Builds a XYCircle2D from XYCircle. Distance calculations are performed using cartesian distance.*/
static Component2D create(XYCircle circle) {
DistanceCalculator calculator = new CartesianDistance(circle.getX(), circle.getY(), circle.getRadius());
return new Circle2D(calculator);
}
/** Builds a Circle2D from Circle. Distance calculations are performed using haversin distance. */
static Component2D create(Circle circle) {
DistanceCalculator calculator = new HaversinDistance(circle.getLon(), circle.getLat(), circle.getRadius());
return new Circle2D(calculator);
}
}

View File

@ -0,0 +1,99 @@
/*
* 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.geo;
import static org.apache.lucene.geo.XYEncodingUtils.checkVal;
/**
* Represents a circle on the XY plane.
* <p>
* NOTES:
* <ol>
* <li> X/Y precision is float.
* <li> Radius precision is float.
* </ol>
* @lucene.experimental
*/
public final class XYCircle extends XYGeometry {
/** Center x */
private final float x;
/** Center y */
private final float y;
/** radius */
private final float radius;
/**
* Creates a new circle from the supplied x/y center and radius.
*/
public XYCircle(float x, float y, float radius) {
if (radius <= 0) {
throw new IllegalArgumentException("radius must be bigger than 0, got " + radius);
}
if (Float.isFinite(radius) == false) {
throw new IllegalArgumentException("radius must be finite, got " + radius);
}
this.x = checkVal(x);
this.y = checkVal(y);
this.radius = radius;
}
/** Returns the center's x */
public float getX() {
return x;
}
/** Returns the center's y */
public float getY() {
return y;
}
/** Returns the radius */
public float getRadius() {
return radius;
}
@Override
protected Component2D toComponent2D() {
return Circle2D.create(this);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof XYCircle)) return false;
XYCircle circle = (XYCircle) o;
return x == circle.x && y == circle.y && radius == circle.radius;
}
@Override
public int hashCode() {
int result = Float.hashCode(x);
result = 31 * result + Float.hashCode(y);
result = 31 * result + Float.hashCode(radius);
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("CIRCLE(");
sb.append("[" + x + "," + y + "]");
sb.append(" radius = " + radius);
sb.append(')');
return sb.toString();
}
}

View File

@ -29,6 +29,7 @@ import org.apache.lucene.geo.Rectangle;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryUtils;
import org.apache.lucene.util.TestUtil;
import org.apache.lucene.geo.Circle;
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude;
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
@ -91,6 +92,21 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase {
return LatLonGeometry.create(pointArray);
}
@Override
protected Query newDistanceQuery(String field, QueryRelation queryRelation, Object circle) {
return LatLonShape.newDistanceQuery(field, queryRelation, (Circle) circle);
}
@Override
protected Component2D toCircle2D(Object circle) {
return LatLonGeometry.create((Circle) circle);
}
@Override
protected Circle nextCircle() {
return new Circle(nextLatitude(), nextLongitude(), random().nextDouble() * Circle.MAX_RADIUS);
}
@Override
public Rectangle randomQueryBox() {
return GeoTestUtil.nextBox();

View File

@ -164,6 +164,8 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
protected abstract Object[] nextPoints();
protected abstract Object nextCircle();
protected abstract double rectMinX(Object rect);
protected abstract double rectMaxX(Object rect);
protected abstract double rectMinY(Object rect);
@ -183,6 +185,10 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
return nextPolygon();
}
protected Object randomQueryCircle() {
return nextCircle();
}
/** factory method to create a new bounding box query */
protected abstract Query newRectQuery(String field, QueryRelation queryRelation, double minX, double maxX, double minY, double maxY);
@ -192,15 +198,20 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
/** factory method to create a new polygon query */
protected abstract Query newPolygonQuery(String field, QueryRelation queryRelation, Object... polygons);
/** factory method to create a new polygon query */
/** factory method to create a new point query */
protected abstract Query newPointsQuery(String field, QueryRelation queryRelation, Object... points);
/** factory method to create a new distance query */
protected abstract Query newDistanceQuery(String field, QueryRelation queryRelation, Object circle);
protected abstract Component2D toLine2D(Object... line);
protected abstract Component2D toPolygon2D(Object... polygon);
protected abstract Component2D toPoint2D(Object... points);
protected abstract Component2D toCircle2D(Object circle);
private void verify(Object... shapes) throws Exception {
IndexWriterConfig iwc = newIndexWriterConfig();
iwc.setMergeScheduler(new SerialMergeScheduler());
@ -261,6 +272,8 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
verifyRandomPolygonQueries(reader, shapes);
// test random point queries
verifyRandomPointQueries(reader, shapes);
// test random distance queries
verifyRandomDistanceQueries(reader, shapes);
}
/** test random generated bounding boxes */
@ -655,6 +668,97 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
}
}
/** test random generated circles */
protected void verifyRandomDistanceQueries(IndexReader reader, Object... shapes) throws Exception {
IndexSearcher s = newSearcher(reader);
final int iters = scaledIterationCount(shapes.length);
Bits liveDocs = MultiBits.getLiveDocs(s.getIndexReader());
int maxDoc = s.getIndexReader().maxDoc();
for (int iter = 0; iter < iters; ++iter) {
if (VERBOSE) {
System.out.println("\nTEST: iter=" + (iter + 1) + " of " + iters + " s=" + s);
}
// Polygon
Object queryCircle = randomQueryCircle();
Component2D queryCircle2D = toCircle2D(queryCircle);
QueryRelation queryRelation = RandomPicks.randomFrom(random(), QueryRelation.values());
Query query = newDistanceQuery(FIELD_NAME, queryRelation, queryCircle);
if (VERBOSE) {
System.out.println(" query=" + query + ", relation=" + queryRelation);
}
final FixedBitSet hits = new FixedBitSet(maxDoc);
s.search(query, new SimpleCollector() {
private int docBase;
@Override
public ScoreMode scoreMode() {
return ScoreMode.COMPLETE_NO_SCORES;
}
@Override
protected void doSetNextReader(LeafReaderContext context) throws IOException {
docBase = context.docBase;
}
@Override
public void collect(int doc) throws IOException {
hits.set(docBase+doc);
}
});
boolean fail = false;
NumericDocValues docIDToID = MultiDocValues.getNumericValues(reader, "id");
for (int docID = 0; docID < maxDoc; ++docID) {
assertEquals(docID, docIDToID.nextDoc());
int id = (int) docIDToID.longValue();
boolean expected;
if (liveDocs != null && liveDocs.get(docID) == false) {
// document is deleted
expected = false;
} else if (shapes[id] == null) {
expected = false;
} else {
expected = VALIDATOR.setRelation(queryRelation).testComponentQuery(queryCircle2D, shapes[id]);
}
if (hits.get(docID) != expected) {
StringBuilder b = new StringBuilder();
if (expected) {
b.append("FAIL: id=" + id + " should match but did not\n");
} else {
b.append("FAIL: id=" + id + " should not match but did\n");
}
b.append(" relation=" + queryRelation + "\n");
b.append(" query=" + query + " docID=" + docID + "\n");
if (shapes[id] instanceof Object[]) {
b.append(" shape=" + Arrays.toString((Object[]) shapes[id]) + "\n");
} else {
b.append(" shape=" + shapes[id] + "\n");
}
b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
b.append(" distanceQuery=" + queryCircle.toString());
if (true) {
fail("wrong hit (first of possibly more):\n\n" + b);
} else {
System.out.println(b.toString());
fail = true;
}
}
}
if (fail) {
fail("some hits were wrong");
}
}
}
protected abstract Validator getValidator();

View File

@ -23,6 +23,7 @@ import com.carrotsearch.randomizedtesting.generators.RandomPicks;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.Component2D;
import org.apache.lucene.geo.ShapeTestUtil;
import org.apache.lucene.geo.XYCircle;
import org.apache.lucene.geo.XYGeometry;
import org.apache.lucene.geo.XYLine;
import org.apache.lucene.geo.XYPoint;
@ -65,6 +66,11 @@ public abstract class BaseXYShapeTestCase extends BaseShapeTestCase {
return XYShape.newPointQuery(field, queryRelation, Arrays.stream(points).toArray(float[][]::new));
}
@Override
protected Query newDistanceQuery(String field, QueryRelation queryRelation, Object circle) {
return XYShape.newDistanceQuery(field, queryRelation, (XYCircle) circle);
}
@Override
protected Component2D toPoint2D(Object... points) {
float[][] p = Arrays.stream(points).toArray(float[][]::new);
@ -85,6 +91,11 @@ public abstract class BaseXYShapeTestCase extends BaseShapeTestCase {
return XYGeometry.create(Arrays.stream(polygons).toArray(XYPolygon[]::new));
}
@Override
protected Component2D toCircle2D(Object circle) {
return XYGeometry.create((XYCircle) circle);
}
@Override
public XYRectangle randomQueryBox() {
return ShapeTestUtil.nextBox(random());
@ -150,6 +161,11 @@ public abstract class BaseXYShapeTestCase extends BaseShapeTestCase {
return points;
}
@Override
protected Object nextCircle() {
return ShapeTestUtil.nextCircle();
}
@Override
protected Encoder getEncoder() {
return new Encoder() {

View File

@ -18,6 +18,7 @@ package org.apache.lucene.document;
import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.Circle;
import org.apache.lucene.geo.Component2D;
import org.apache.lucene.geo.GeoEncodingUtils;
import org.apache.lucene.geo.GeoTestUtil;
@ -409,8 +410,8 @@ public class TestLatLonShape extends LuceneTestCase {
byte[] encoded = new byte[7 * ShapeField.BYTES];
ShapeField.encodeTriangle(encoded, encodeLatitude(t.getY(0)), encodeLongitude(t.getX(0)), t.isEdgefromPolygon(0),
encodeLatitude(t.getY(1)), encodeLongitude(t.getX(1)), t.isEdgefromPolygon(1),
encodeLatitude(t.getY(2)), encodeLongitude(t.getX(2)), t.isEdgefromPolygon(2));
encodeLatitude(t.getY(1)), encodeLongitude(t.getX(1)), t.isEdgefromPolygon(1),
encodeLatitude(t.getY(2)), encodeLongitude(t.getX(2)), t.isEdgefromPolygon(2));
ShapeField.DecodedTriangle decoded = new ShapeField.DecodedTriangle();
ShapeField.decodeTriangle(encoded, decoded);
@ -740,4 +741,51 @@ public class TestLatLonShape extends LuceneTestCase {
IOUtils.close(w, reader, dir);
}
public void testPointIndexAndDistanceQuery() throws Exception {
Directory dir = newDirectory();
RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
Document document = new Document();
BaseLatLonShapeTestCase.Point p = (BaseLatLonShapeTestCase.Point) BaseLatLonShapeTestCase.ShapeType.POINT.nextShape();
Field[] fields = LatLonShape.createIndexableFields(FIELDNAME, p.lat,p.lon);
for (Field f : fields) {
document.add(f);
}
writer.addDocument(document);
//// search
IndexReader r = writer.getReader();
writer.close();
IndexSearcher s = newSearcher(r);
double lat = GeoTestUtil.nextLatitude();
double lon = GeoTestUtil.nextLongitude();
double radiusMeters = random().nextDouble() * Circle.MAX_RADIUS;
while (radiusMeters == 0 || radiusMeters == Circle.MAX_RADIUS) {
radiusMeters = random().nextDouble() * Circle.MAX_RADIUS;
}
Circle circle = new Circle(lat, lon, radiusMeters);
Component2D circle2D = LatLonGeometry.create(circle);
int expected;
int expectedDisjoint;
if (circle2D.contains(p.lon, p.lat)) {
expected = 1;
expectedDisjoint = 0;
} else {
expected = 0;
expectedDisjoint = 1;
}
Query q = LatLonShape.newDistanceQuery(FIELDNAME, QueryRelation.INTERSECTS, circle);
assertEquals(expected, s.count(q));
q = LatLonShape.newDistanceQuery(FIELDNAME, QueryRelation.WITHIN, circle);
assertEquals(expected, s.count(q));
q = LatLonShape.newDistanceQuery(FIELDNAME, QueryRelation.DISJOINT, circle);
assertEquals(expectedDisjoint, s.count(q));
IOUtils.close(r, dir);
}
}

View File

@ -19,8 +19,11 @@ package org.apache.lucene.document;
import java.util.Random;
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.geo.Component2D;
import org.apache.lucene.geo.ShapeTestUtil;
import org.apache.lucene.geo.Tessellator;
import org.apache.lucene.geo.XYCircle;
import org.apache.lucene.geo.XYGeometry;
import org.apache.lucene.geo.XYLine;
import org.apache.lucene.geo.XYPolygon;
import org.apache.lucene.geo.XYRectangle;
@ -159,6 +162,46 @@ public class TestXYShape extends LuceneTestCase {
IOUtils.close(reader, dir);
}
public void testPointIndexAndDistanceQuery() throws Exception {
Directory dir = newDirectory();
RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
Document document = new Document();
float pX = ShapeTestUtil.nextFloat(random());
float py = ShapeTestUtil.nextFloat(random());
Field[] fields = XYShape.createIndexableFields(FIELDNAME, pX, py);
for (Field f : fields) {
document.add(f);
}
writer.addDocument(document);
//// search
IndexReader r = writer.getReader();
writer.close();
IndexSearcher s = newSearcher(r);
XYCircle circle = ShapeTestUtil.nextCircle();
Component2D circle2D = XYGeometry.create(circle);
int expected;
int expectedDisjoint;
if (circle2D.contains(pX, py)) {
expected = 1;
expectedDisjoint = 0;
} else {
expected = 0;
expectedDisjoint = 1;
}
Query q = XYShape.newDistanceQuery(FIELDNAME, QueryRelation.INTERSECTS, circle);
assertEquals(expected, s.count(q));
q = XYShape.newDistanceQuery(FIELDNAME, QueryRelation.WITHIN, circle);
assertEquals(expected, s.count(q));
q = XYShape.newDistanceQuery(FIELDNAME, QueryRelation.DISJOINT, circle);
assertEquals(expectedDisjoint, s.count(q));
IOUtils.close(r, dir);
}
private static boolean areBoxDisjoint(XYRectangle r1, XYRectangle r2) {
return ( r1.minX <= r2.minX && r1.minY <= r2.minY && r1.maxX >= r2.maxX && r1.maxY >= r2.maxY);
}

View File

@ -68,6 +68,14 @@ public class ShapeTestUtil {
return new XYLine(x, y);
}
public static XYCircle nextCircle() {
Random random = random();
float x = nextFloat(random);
float y = nextFloat(random);
float radius = random().nextFloat() * Float.MAX_VALUE / 2;
return new XYCircle(x, y, radius);
}
private static XYPolygon trianglePolygon(XYRectangle box) {
final float[] polyX = new float[4];
final float[] polyY = new float[4];

View File

@ -0,0 +1,70 @@
/*
* 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.geo;
import org.apache.lucene.util.LuceneTestCase;
public class TestCircle extends LuceneTestCase {
/** latitude should be on range */
public void testInvalidLat() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
new Circle(134.14, 45.23, 1000);
});
assertTrue(expected.getMessage().contains("invalid latitude 134.14; must be between -90.0 and 90.0"));
}
/** longitude should be on range */
public void testInvalidLon() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
new Circle(43.5, 180.5, 1000);
});
assertTrue(expected.getMessage().contains("invalid longitude 180.5; must be between -180.0 and 180.0"));
}
/** radius must be positive */
public void testNegativeRadius() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
new Circle(43.5, 45.23, -1000);
});
assertTrue(expected.getMessage().contains("radius must be bigger than 0, got -1000.0"));
}
/** radius must be lower than 3185504.3857 */
public void testInfiniteRadius() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
new Circle(43.5, 45.23, Double.POSITIVE_INFINITY);
});
assertTrue(expected.getMessage().contains("radius must be lower than 3185504.3857, got Infinity"));
}
/** equals and hashcode */
public void testEqualsAndHashCode() {
Circle circle = GeoTestUtil.nextCircle();
Circle copy = new Circle(circle.getLat(), circle.getLon(), circle.getRadius());
assertEquals(circle, copy);
assertEquals(circle.hashCode(), copy.hashCode());
Circle otherCircle = GeoTestUtil.nextCircle();
if (circle.getLon() != otherCircle.getLon() || circle.getLat() != otherCircle.getLat() || circle.getRadius() != otherCircle.getRadius()) {
assertNotEquals(circle, otherCircle);
assertNotEquals(circle.hashCode(), otherCircle.hashCode());
} else {
assertEquals(circle, otherCircle);
assertEquals(circle.hashCode(), otherCircle.hashCode());
}
}
}

View File

@ -0,0 +1,134 @@
/*
* 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.geo;
import org.apache.lucene.index.PointValues;
import org.apache.lucene.util.LuceneTestCase;
public class TestCircle2D extends LuceneTestCase {
public void testTriangleDisjoint() {
Component2D circle2D;
if (random().nextBoolean()) {
Circle circle = new Circle(0, 0, 100);
circle2D = LatLonGeometry.create(circle);
} else {
XYCircle xyCircle = new XYCircle(0, 0, 1);
circle2D = XYGeometry.create(xyCircle);
}
double ax = 4;
double ay = 4;
double bx = 5;
double by = 5;
double cx = 5;
double cy = 4;
assertEquals(PointValues.Relation.CELL_OUTSIDE_QUERY, circle2D.relateTriangle(ax, ay, bx, by , cx, cy));
assertEquals(Component2D.WithinRelation.DISJOINT, circle2D.withinTriangle(ax, ay, true, bx, by, true, cx, cy, true));
}
public void testTriangleIntersects() {
Component2D circle2D;
if (random().nextBoolean()) {
Circle circle = new Circle(0, 0, 1000000);
circle2D = LatLonGeometry.create(circle);
} else {
XYCircle xyCircle = new XYCircle(0, 0, 10);
circle2D = XYGeometry.create(xyCircle);
}
double ax = -20;
double ay = 1;
double bx = 20;
double by = 1;
double cx = 0;
double cy = 90;
assertEquals(PointValues.Relation.CELL_CROSSES_QUERY, circle2D.relateTriangle(ax, ay, bx, by , cx, cy));
assertEquals(Component2D.WithinRelation.NOTWITHIN, circle2D.withinTriangle(ax, ay, true, bx, by, true, cx, cy, true));
}
public void testTriangleContains() {
Component2D circle2D;
if (random().nextBoolean()) {
Circle circle = new Circle(0, 0, 1000000);
circle2D = LatLonGeometry.create(circle);
} else {
XYCircle xyCircle = new XYCircle(0, 0, 1);
circle2D = XYGeometry.create(xyCircle);
}
double ax = 0.25;
double ay = 0.25;
double bx = 0.5;
double by = 0.5;
double cx = 0.5;
double cy = 0.25;
assertEquals(PointValues.Relation.CELL_INSIDE_QUERY, circle2D.relateTriangle(ax, ay, bx, by , cx, cy));
assertEquals(Component2D.WithinRelation.NOTWITHIN, circle2D.withinTriangle(ax, ay, true, bx, by, true, cx, cy, true));
}
public void testTriangleWithin() {
Component2D circle2D;
if (random().nextBoolean()) {
Circle circle = new Circle(0, 0, 1000);
circle2D = LatLonGeometry.create(circle);
} else {
XYCircle xyCircle = new XYCircle(0, 0, 1);
circle2D = XYGeometry.create(xyCircle);
}
double ax = -20;
double ay = -20;
double bx = 20;
double by = -20;
double cx = 0;
double cy = 20;
assertEquals(PointValues.Relation.CELL_CROSSES_QUERY, circle2D.relateTriangle(ax, ay, bx, by , cx, cy));
assertEquals(Component2D.WithinRelation.CANDIDATE, circle2D.withinTriangle(ax, ay, true, bx, by, true, cx, cy, true));
}
public void testRandomTriangles() {
Component2D circle2D;
if (random().nextBoolean()) {
Circle circle = GeoTestUtil.nextCircle();
circle2D = LatLonGeometry.create(circle);
} else {
XYCircle circle = ShapeTestUtil.nextCircle();
circle2D = XYGeometry.create(circle);
}
for (int i =0; i < 100; i++) {
double ax = GeoTestUtil.nextLongitude();
double ay = GeoTestUtil.nextLatitude();
double bx = GeoTestUtil.nextLongitude();
double by = GeoTestUtil.nextLatitude();
double cx = GeoTestUtil.nextLongitude();
double cy = GeoTestUtil.nextLatitude();
double tMinX = StrictMath.min(StrictMath.min(ax, bx), cx);
double tMaxX = StrictMath.max(StrictMath.max(ax, bx), cx);
double tMinY = StrictMath.min(StrictMath.min(ay, by), cy);
double tMaxY = StrictMath.max(StrictMath.max(ay, by), cy);
PointValues.Relation r = circle2D.relate(tMinX, tMaxX, tMinY, tMaxY);
if (r == PointValues.Relation.CELL_OUTSIDE_QUERY) {
assertEquals(PointValues.Relation.CELL_OUTSIDE_QUERY, circle2D.relateTriangle(ax, ay, bx, by , cx, cy));
assertEquals(Component2D.WithinRelation.DISJOINT, circle2D.withinTriangle(ax, ay, true, bx, by, true, cx, cy, true));
} else if (r == PointValues.Relation.CELL_INSIDE_QUERY) {
assertEquals(PointValues.Relation.CELL_CROSSES_QUERY, circle2D.relateTriangle(ax, ay, bx, by , cx, cy));
assertEquals(Component2D.WithinRelation.NOTWITHIN, circle2D.withinTriangle(ax, ay, true, bx, by, true, cx, cy, true));
}
}
}
}

View File

@ -0,0 +1,93 @@
/*
* 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.geo;
import org.apache.lucene.util.LuceneTestCase;
public class TestXYCircle extends LuceneTestCase {
/** point values cannot be NaN */
public void testNaN() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
new XYCircle(Float.NaN, 45.23f, 35.5f);
});
assertTrue(expected.getMessage().contains("invalid value NaN"));
expected = expectThrows(IllegalArgumentException.class, () -> {
new XYCircle(43.5f, Float.NaN, 35.5f);
});
assertTrue(expected.getMessage(), expected.getMessage().contains("invalid value NaN"));
}
/** point values mist be finite */
public void testPositiveInf() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
new XYCircle(Float.POSITIVE_INFINITY, 45.23f, 35.5f);
});
assertTrue(expected.getMessage().contains("invalid value Inf"));
expected = expectThrows(IllegalArgumentException.class, () -> {
new XYCircle(43.5f, Float.POSITIVE_INFINITY, 35.5f);
});
assertTrue(expected.getMessage(), expected.getMessage().contains("invalid value Inf"));
}
/** point values mist be finite */
public void testNegativeInf() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
new XYCircle(Float.NEGATIVE_INFINITY, 45.23f, 35.5f);
});
assertTrue(expected.getMessage().contains("invalid value -Inf"));
expected = expectThrows(IllegalArgumentException.class, () -> {
new XYCircle(43.5f, Float.NEGATIVE_INFINITY, 35.5f);
});
assertTrue(expected.getMessage(), expected.getMessage().contains("invalid value -Inf"));
}
/** radius must be positive */
public void testNegativeRadius() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
new XYCircle(43.5f, 45.23f, -1000f);
});
assertTrue(expected.getMessage(), expected.getMessage().contains("radius must be bigger than 0, got -1000.0"));
}
/** radius must be lower than 3185504.3857 */
public void testInfiniteRadius() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
new XYCircle(43.5f, 45.23f, Float.POSITIVE_INFINITY);
});
assertTrue(expected.getMessage(), expected.getMessage().contains("radius must be finite, got Infinity"));
}
/** equals and hashcode */
public void testEqualsAndHashCode() {
XYCircle circle = ShapeTestUtil.nextCircle();
XYCircle copy = new XYCircle(circle.getX(), circle.getY(), circle.getRadius());
assertEquals(circle, copy);
assertEquals(circle.hashCode(), copy.hashCode());
XYCircle otherCircle = ShapeTestUtil.nextCircle();
if (circle.getX() != otherCircle.getX() || circle.getY() != otherCircle.getY() || circle.getRadius() != otherCircle.getRadius()) {
assertNotEquals(circle, otherCircle);
assertNotEquals(circle.hashCode(), otherCircle.hashCode());
} else {
assertEquals(circle, otherCircle);
assertEquals(circle.hashCode(), otherCircle.hashCode());
}
}
}

View File

@ -384,6 +384,13 @@ public class GeoTestUtil {
return new Polygon(result[0], result[1]);
}
public static Circle nextCircle() {
double lat = nextLatitude();
double lon = nextLongitude();
double radiusMeters = random().nextDouble() * Circle.MAX_RADIUS;
return new Circle(lat, lon, radiusMeters);
}
/** returns next pseudorandom polygon */
public static Polygon nextPolygon() {
if (random().nextBoolean()) {