diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 414dd8e23d4..733e3674076 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -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
---------------------
diff --git a/lucene/core/src/java/org/apache/lucene/document/LatLonShape.java b/lucene/core/src/java/org/apache/lucene/document/LatLonShape.java
index 3c4c39c2b32..a583e2cc65f 100644
--- a/lucene/core/src/java/org/apache/lucene/document/LatLonShape.java
+++ b/lucene/core/src/java/org/apache/lucene/document/LatLonShape.java
@@ -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);
}
+
}
diff --git a/lucene/core/src/java/org/apache/lucene/document/XYShape.java b/lucene/core/src/java/org/apache/lucene/document/XYShape.java
index 88f9a5d5d7e..7eb84030ad1 100644
--- a/lucene/core/src/java/org/apache/lucene/document/XYShape.java
+++ b/lucene/core/src/java/org/apache/lucene/document/XYShape.java
@@ -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
**/
diff --git a/lucene/core/src/java/org/apache/lucene/geo/Circle.java b/lucene/core/src/java/org/apache/lucene/geo/Circle.java
new file mode 100644
index 00000000000..4bb63c845b4
--- /dev/null
+++ b/lucene/core/src/java/org/apache/lucene/geo/Circle.java
@@ -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.
+ *
+ * NOTES:
+ *
+ * - Latitude/longitude values must be in decimal degrees.
+ *
- Radius must be in meters.
+ *
- For more advanced GeoSpatial indexing and query operations see the {@code spatial-extras} module
+ *
+ * @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();
+ }
+}
diff --git a/lucene/core/src/java/org/apache/lucene/geo/Circle2D.java b/lucene/core/src/java/org/apache/lucene/geo/Circle2D.java
new file mode 100644
index 00000000000..18bc587010f
--- /dev/null
+++ b/lucene/core/src/java/org/apache/lucene/geo/Circle2D.java
@@ -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);
+ }
+}
diff --git a/lucene/core/src/java/org/apache/lucene/geo/XYCircle.java b/lucene/core/src/java/org/apache/lucene/geo/XYCircle.java
new file mode 100644
index 00000000000..b31267317cc
--- /dev/null
+++ b/lucene/core/src/java/org/apache/lucene/geo/XYCircle.java
@@ -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.
+ *
+ * NOTES:
+ *
+ * - X/Y precision is float.
+ *
- Radius precision is float.
+ *
+ * @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();
+ }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java
index c49496a7f2f..f15062a1786 100644
--- a/lucene/core/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java
+++ b/lucene/core/src/test/org/apache/lucene/document/BaseLatLonShapeTestCase.java
@@ -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();
diff --git a/lucene/core/src/test/org/apache/lucene/document/BaseShapeTestCase.java b/lucene/core/src/test/org/apache/lucene/document/BaseShapeTestCase.java
index 21ed1216b10..16f7a21fca0 100644
--- a/lucene/core/src/test/org/apache/lucene/document/BaseShapeTestCase.java
+++ b/lucene/core/src/test/org/apache/lucene/document/BaseShapeTestCase.java
@@ -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();
diff --git a/lucene/core/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java b/lucene/core/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java
index ef34cfe0765..b13974d022a 100644
--- a/lucene/core/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java
+++ b/lucene/core/src/test/org/apache/lucene/document/BaseXYShapeTestCase.java
@@ -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() {
diff --git a/lucene/core/src/test/org/apache/lucene/document/TestLatLonShape.java b/lucene/core/src/test/org/apache/lucene/document/TestLatLonShape.java
index 8dd5486779d..7b0c0fc2c77 100644
--- a/lucene/core/src/test/org/apache/lucene/document/TestLatLonShape.java
+++ b/lucene/core/src/test/org/apache/lucene/document/TestLatLonShape.java
@@ -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);
+ }
}
diff --git a/lucene/core/src/test/org/apache/lucene/document/TestXYShape.java b/lucene/core/src/test/org/apache/lucene/document/TestXYShape.java
index fad04cbaf38..5cfdf072cc9 100644
--- a/lucene/core/src/test/org/apache/lucene/document/TestXYShape.java
+++ b/lucene/core/src/test/org/apache/lucene/document/TestXYShape.java
@@ -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);
}
diff --git a/lucene/core/src/test/org/apache/lucene/geo/ShapeTestUtil.java b/lucene/core/src/test/org/apache/lucene/geo/ShapeTestUtil.java
index a8d08f4d11a..a3c7f2aa375 100644
--- a/lucene/core/src/test/org/apache/lucene/geo/ShapeTestUtil.java
+++ b/lucene/core/src/test/org/apache/lucene/geo/ShapeTestUtil.java
@@ -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];
diff --git a/lucene/core/src/test/org/apache/lucene/geo/TestCircle.java b/lucene/core/src/test/org/apache/lucene/geo/TestCircle.java
new file mode 100644
index 00000000000..8005d728e8f
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/geo/TestCircle.java
@@ -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());
+ }
+ }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/geo/TestCircle2D.java b/lucene/core/src/test/org/apache/lucene/geo/TestCircle2D.java
new file mode 100644
index 00000000000..2ad5c2130c2
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/geo/TestCircle2D.java
@@ -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));
+ }
+ }
+ }
+}
diff --git a/lucene/core/src/test/org/apache/lucene/geo/TestXYCircle.java b/lucene/core/src/test/org/apache/lucene/geo/TestXYCircle.java
new file mode 100644
index 00000000000..a6400bb382c
--- /dev/null
+++ b/lucene/core/src/test/org/apache/lucene/geo/TestXYCircle.java
@@ -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());
+ }
+ }
+}
\ No newline at end of file
diff --git a/lucene/test-framework/src/java/org/apache/lucene/geo/GeoTestUtil.java b/lucene/test-framework/src/java/org/apache/lucene/geo/GeoTestUtil.java
index 243e2f12010..be831ef37f9 100644
--- a/lucene/test-framework/src/java/org/apache/lucene/geo/GeoTestUtil.java
+++ b/lucene/test-framework/src/java/org/apache/lucene/geo/GeoTestUtil.java
@@ -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()) {