mirror of https://github.com/apache/lucene.git
LUCENE-8707: Add LatLonShape and XYShape distance query (#587)
This commit is contained in:
parent
001a35cc06
commit
d48bafb299
|
@ -117,7 +117,9 @@ API Changes
|
||||||
New Features
|
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
|
Improvements
|
||||||
---------------------
|
---------------------
|
||||||
|
|
|
@ -21,6 +21,7 @@ import java.util.List;
|
||||||
|
|
||||||
import org.apache.lucene.document.ShapeField.QueryRelation; // javadoc
|
import org.apache.lucene.document.ShapeField.QueryRelation; // javadoc
|
||||||
import org.apache.lucene.document.ShapeField.Triangle;
|
import org.apache.lucene.document.ShapeField.Triangle;
|
||||||
|
import org.apache.lucene.geo.Circle;
|
||||||
import org.apache.lucene.geo.GeoUtils;
|
import org.apache.lucene.geo.GeoUtils;
|
||||||
import org.apache.lucene.geo.LatLonGeometry;
|
import org.apache.lucene.geo.LatLonGeometry;
|
||||||
import org.apache.lucene.geo.Line;
|
import org.apache.lucene.geo.Line;
|
||||||
|
@ -131,6 +132,11 @@ public class LatLonShape {
|
||||||
return newGeometryQuery(field, queryRelation, pointArray);
|
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).
|
/** 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) {
|
public static Query newGeometryQuery(String field, QueryRelation queryRelation, LatLonGeometry... latLonGeometries) {
|
||||||
|
@ -143,4 +149,5 @@ public class LatLonShape {
|
||||||
}
|
}
|
||||||
return new LatLonShapeQuery(field, queryRelation, latLonGeometries);
|
return new LatLonShapeQuery(field, queryRelation, latLonGeometries);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import java.util.List;
|
||||||
import org.apache.lucene.document.ShapeField.QueryRelation; // javadoc
|
import org.apache.lucene.document.ShapeField.QueryRelation; // javadoc
|
||||||
import org.apache.lucene.document.ShapeField.Triangle;
|
import org.apache.lucene.document.ShapeField.Triangle;
|
||||||
import org.apache.lucene.geo.Tessellator;
|
import org.apache.lucene.geo.Tessellator;
|
||||||
|
import org.apache.lucene.geo.XYCircle;
|
||||||
import org.apache.lucene.geo.XYGeometry;
|
import org.apache.lucene.geo.XYGeometry;
|
||||||
import org.apache.lucene.geo.XYPoint;
|
import org.apache.lucene.geo.XYPoint;
|
||||||
import org.apache.lucene.geo.XYRectangle;
|
import org.apache.lucene.geo.XYRectangle;
|
||||||
|
@ -117,6 +118,11 @@ public class XYShape {
|
||||||
return newGeometryQuery(field, queryRelation, pointArray);
|
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
|
/** create a query to find all indexed geo shapes that intersect a provided geometry collection
|
||||||
* note: Components do not support dateline crossing
|
* note: Components do not support dateline crossing
|
||||||
**/
|
**/
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ import org.apache.lucene.geo.Rectangle;
|
||||||
import org.apache.lucene.search.Query;
|
import org.apache.lucene.search.Query;
|
||||||
import org.apache.lucene.search.QueryUtils;
|
import org.apache.lucene.search.QueryUtils;
|
||||||
import org.apache.lucene.util.TestUtil;
|
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.decodeLatitude;
|
||||||
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
|
import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude;
|
||||||
|
@ -91,6 +92,21 @@ public abstract class BaseLatLonShapeTestCase extends BaseShapeTestCase {
|
||||||
return LatLonGeometry.create(pointArray);
|
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
|
@Override
|
||||||
public Rectangle randomQueryBox() {
|
public Rectangle randomQueryBox() {
|
||||||
return GeoTestUtil.nextBox();
|
return GeoTestUtil.nextBox();
|
||||||
|
|
|
@ -164,6 +164,8 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
|
||||||
|
|
||||||
protected abstract Object[] nextPoints();
|
protected abstract Object[] nextPoints();
|
||||||
|
|
||||||
|
protected abstract Object nextCircle();
|
||||||
|
|
||||||
protected abstract double rectMinX(Object rect);
|
protected abstract double rectMinX(Object rect);
|
||||||
protected abstract double rectMaxX(Object rect);
|
protected abstract double rectMaxX(Object rect);
|
||||||
protected abstract double rectMinY(Object rect);
|
protected abstract double rectMinY(Object rect);
|
||||||
|
@ -183,6 +185,10 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
|
||||||
return nextPolygon();
|
return nextPolygon();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Object randomQueryCircle() {
|
||||||
|
return nextCircle();
|
||||||
|
}
|
||||||
|
|
||||||
/** factory method to create a new bounding box query */
|
/** 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);
|
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 */
|
/** factory method to create a new polygon query */
|
||||||
protected abstract Query newPolygonQuery(String field, QueryRelation queryRelation, Object... polygons);
|
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);
|
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 toLine2D(Object... line);
|
||||||
|
|
||||||
protected abstract Component2D toPolygon2D(Object... polygon);
|
protected abstract Component2D toPolygon2D(Object... polygon);
|
||||||
|
|
||||||
protected abstract Component2D toPoint2D(Object... points);
|
protected abstract Component2D toPoint2D(Object... points);
|
||||||
|
|
||||||
|
protected abstract Component2D toCircle2D(Object circle);
|
||||||
|
|
||||||
private void verify(Object... shapes) throws Exception {
|
private void verify(Object... shapes) throws Exception {
|
||||||
IndexWriterConfig iwc = newIndexWriterConfig();
|
IndexWriterConfig iwc = newIndexWriterConfig();
|
||||||
iwc.setMergeScheduler(new SerialMergeScheduler());
|
iwc.setMergeScheduler(new SerialMergeScheduler());
|
||||||
|
@ -261,6 +272,8 @@ public abstract class BaseShapeTestCase extends LuceneTestCase {
|
||||||
verifyRandomPolygonQueries(reader, shapes);
|
verifyRandomPolygonQueries(reader, shapes);
|
||||||
// test random point queries
|
// test random point queries
|
||||||
verifyRandomPointQueries(reader, shapes);
|
verifyRandomPointQueries(reader, shapes);
|
||||||
|
// test random distance queries
|
||||||
|
verifyRandomDistanceQueries(reader, shapes);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** test random generated bounding boxes */
|
/** 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();
|
protected abstract Validator getValidator();
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import com.carrotsearch.randomizedtesting.generators.RandomPicks;
|
||||||
import org.apache.lucene.document.ShapeField.QueryRelation;
|
import org.apache.lucene.document.ShapeField.QueryRelation;
|
||||||
import org.apache.lucene.geo.Component2D;
|
import org.apache.lucene.geo.Component2D;
|
||||||
import org.apache.lucene.geo.ShapeTestUtil;
|
import org.apache.lucene.geo.ShapeTestUtil;
|
||||||
|
import org.apache.lucene.geo.XYCircle;
|
||||||
import org.apache.lucene.geo.XYGeometry;
|
import org.apache.lucene.geo.XYGeometry;
|
||||||
import org.apache.lucene.geo.XYLine;
|
import org.apache.lucene.geo.XYLine;
|
||||||
import org.apache.lucene.geo.XYPoint;
|
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));
|
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
|
@Override
|
||||||
protected Component2D toPoint2D(Object... points) {
|
protected Component2D toPoint2D(Object... points) {
|
||||||
float[][] p = Arrays.stream(points).toArray(float[][]::new);
|
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));
|
return XYGeometry.create(Arrays.stream(polygons).toArray(XYPolygon[]::new));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Component2D toCircle2D(Object circle) {
|
||||||
|
return XYGeometry.create((XYCircle) circle);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public XYRectangle randomQueryBox() {
|
public XYRectangle randomQueryBox() {
|
||||||
return ShapeTestUtil.nextBox(random());
|
return ShapeTestUtil.nextBox(random());
|
||||||
|
@ -150,6 +161,11 @@ public abstract class BaseXYShapeTestCase extends BaseShapeTestCase {
|
||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Object nextCircle() {
|
||||||
|
return ShapeTestUtil.nextCircle();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Encoder getEncoder() {
|
protected Encoder getEncoder() {
|
||||||
return new Encoder() {
|
return new Encoder() {
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.apache.lucene.document;
|
||||||
|
|
||||||
import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
|
import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
|
||||||
import org.apache.lucene.document.ShapeField.QueryRelation;
|
import org.apache.lucene.document.ShapeField.QueryRelation;
|
||||||
|
import org.apache.lucene.geo.Circle;
|
||||||
import org.apache.lucene.geo.Component2D;
|
import org.apache.lucene.geo.Component2D;
|
||||||
import org.apache.lucene.geo.GeoEncodingUtils;
|
import org.apache.lucene.geo.GeoEncodingUtils;
|
||||||
import org.apache.lucene.geo.GeoTestUtil;
|
import org.apache.lucene.geo.GeoTestUtil;
|
||||||
|
@ -409,8 +410,8 @@ public class TestLatLonShape extends LuceneTestCase {
|
||||||
|
|
||||||
byte[] encoded = new byte[7 * ShapeField.BYTES];
|
byte[] encoded = new byte[7 * ShapeField.BYTES];
|
||||||
ShapeField.encodeTriangle(encoded, encodeLatitude(t.getY(0)), encodeLongitude(t.getX(0)), t.isEdgefromPolygon(0),
|
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(1)), encodeLongitude(t.getX(1)), t.isEdgefromPolygon(1),
|
||||||
encodeLatitude(t.getY(2)), encodeLongitude(t.getX(2)), t.isEdgefromPolygon(2));
|
encodeLatitude(t.getY(2)), encodeLongitude(t.getX(2)), t.isEdgefromPolygon(2));
|
||||||
ShapeField.DecodedTriangle decoded = new ShapeField.DecodedTriangle();
|
ShapeField.DecodedTriangle decoded = new ShapeField.DecodedTriangle();
|
||||||
ShapeField.decodeTriangle(encoded, decoded);
|
ShapeField.decodeTriangle(encoded, decoded);
|
||||||
|
|
||||||
|
@ -740,4 +741,51 @@ public class TestLatLonShape extends LuceneTestCase {
|
||||||
|
|
||||||
IOUtils.close(w, reader, dir);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,8 +19,11 @@ package org.apache.lucene.document;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
import org.apache.lucene.document.ShapeField.QueryRelation;
|
import org.apache.lucene.document.ShapeField.QueryRelation;
|
||||||
|
import org.apache.lucene.geo.Component2D;
|
||||||
import org.apache.lucene.geo.ShapeTestUtil;
|
import org.apache.lucene.geo.ShapeTestUtil;
|
||||||
import org.apache.lucene.geo.Tessellator;
|
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.XYLine;
|
||||||
import org.apache.lucene.geo.XYPolygon;
|
import org.apache.lucene.geo.XYPolygon;
|
||||||
import org.apache.lucene.geo.XYRectangle;
|
import org.apache.lucene.geo.XYRectangle;
|
||||||
|
@ -159,6 +162,46 @@ public class TestXYShape extends LuceneTestCase {
|
||||||
IOUtils.close(reader, dir);
|
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) {
|
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);
|
return ( r1.minX <= r2.minX && r1.minY <= r2.minY && r1.maxX >= r2.maxX && r1.maxY >= r2.maxY);
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,6 +68,14 @@ public class ShapeTestUtil {
|
||||||
return new XYLine(x, y);
|
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) {
|
private static XYPolygon trianglePolygon(XYRectangle box) {
|
||||||
final float[] polyX = new float[4];
|
final float[] polyX = new float[4];
|
||||||
final float[] polyY = new float[4];
|
final float[] polyY = new float[4];
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -384,6 +384,13 @@ public class GeoTestUtil {
|
||||||
return new Polygon(result[0], result[1]);
|
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 */
|
/** returns next pseudorandom polygon */
|
||||||
public static Polygon nextPolygon() {
|
public static Polygon nextPolygon() {
|
||||||
if (random().nextBoolean()) {
|
if (random().nextBoolean()) {
|
||||||
|
|
Loading…
Reference in New Issue