LUCENE-7153: give GeoPointField and LatLonPoint full polygon support

This commit is contained in:
Robert Muir 2016-03-31 22:28:46 -04:00
parent 2af578e167
commit 81c83b4431
15 changed files with 735 additions and 533 deletions

View File

@ -46,6 +46,10 @@ Optimizations
* LUCENE-7147: Improve disjoint check for geo distance query traversal
(Ryan Ernst, Robert Muir, Mike McCandless)
* LUCENE-7153: GeoPointField and LatLonPoint polygon queries now support
multiple polygons and holes, with memory usage independent of
polygon complexity. (Karl Wright, Mike McCandless, Robert Muir)
Bug Fixes
* LUCENE-7127: Fix corner case bugs in GeoPointDistanceQuery. (Robert Muir)

View File

@ -29,6 +29,7 @@ import org.apache.lucene.search.PointRangeQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SortField;
import org.apache.lucene.spatial.util.GeoUtils;
import org.apache.lucene.spatial.util.Polygon;
/**
* An indexed location field.
@ -302,18 +303,14 @@ public class LatLonPoint extends Field {
/**
* Create a query for matching a polygon.
* <p>
* The supplied {@code polyLats}/{@code polyLons} must be clockwise or counter-clockwise.
* The supplied {@code polygon} must be clockwise or counter-clockwise.
* @param field field name. must not be null.
* @param polyLats latitude values for points of the polygon: must be within standard +/-90 coordinate bounds.
* @param polyLons longitude values for points of the polygon: must be within standard +/-180 coordinate bounds.
* @param polygons array of polygons. must not be null or empty
* @return query matching points within this polygon
* @throws IllegalArgumentException if {@code field} is null, {@code polyLats} is null or has invalid coordinates,
* {@code polyLons} is null or has invalid coordinates, if {@code polyLats} has a different
* length than {@code polyLons}, if the polygon has less than 4 points, or if polygon is
* not closed (first and last points should be the same)
* @throws IllegalArgumentException if {@code field} is null, {@code polygons} is null or empty
*/
public static Query newPolygonQuery(String field, double[] polyLats, double[] polyLons) {
return new LatLonPointInPolygonQuery(field, polyLats, polyLons);
public static Query newPolygonQuery(String field, Polygon... polygons) {
return new LatLonPointInPolygonQuery(field, polygons);
}
/**

View File

@ -39,12 +39,13 @@ import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.util.BitSet;
import org.apache.lucene.util.DocIdSetBuilder;
import org.apache.lucene.util.FixedBitSet;
import org.apache.lucene.util.NumericUtils;
import org.apache.lucene.util.SparseFixedBitSet;
import org.apache.lucene.util.StringHelper;
import org.apache.lucene.spatial.util.GeoRect;
import org.apache.lucene.spatial.util.GeoRelationUtils;
import org.apache.lucene.spatial.util.GeoUtils;
import org.apache.lucene.spatial.util.Polygon;
/** Finds all previously indexed points that fall within the specified polygon.
/** Finds all previously indexed points that fall within the specified polygons.
*
* <p>The field must be indexed with using {@link org.apache.lucene.document.LatLonPoint} added per document.
*
@ -52,29 +53,27 @@ import org.apache.lucene.spatial.util.GeoUtils;
final class LatLonPointInPolygonQuery extends Query {
final String field;
final double minLat;
final double maxLat;
final double minLon;
final double maxLon;
final double[] polyLats;
final double[] polyLons;
final Polygon[] polygons;
/** The lats/lons must be clockwise or counter-clockwise. */
public LatLonPointInPolygonQuery(String field, double[] polyLats, double[] polyLons) {
public LatLonPointInPolygonQuery(String field, Polygon[] polygons) {
if (field == null) {
throw new IllegalArgumentException("field must not be null");
}
GeoUtils.checkPolygon(polyLats, polyLons);
if (polygons == null) {
throw new IllegalArgumentException("polygons must not be null");
}
if (polygons.length == 0) {
throw new IllegalArgumentException("polygons must not be empty");
}
for (int i = 0; i < polygons.length; i++) {
if (polygons[i] == null) {
throw new IllegalArgumentException("polygon[" + i + "] must not be null");
}
}
this.field = field;
this.polyLats = polyLats;
this.polyLons = polyLons;
this.polygons = polygons.clone();
// TODO: we could also compute the maximal inner bounding box, to make relations faster to compute?
GeoRect box = GeoUtils.polyToBBox(polyLats, polyLons);
this.minLon = box.minLon;
this.maxLon = box.maxLon;
this.minLat = box.minLat;
this.maxLat = box.maxLat;
}
@Override
@ -82,6 +81,25 @@ final class LatLonPointInPolygonQuery extends Query {
// I don't use RandomAccessWeight here: it's no good to approximate with "match all docs"; this is an inverted structure and should be
// used in the first pass:
// bounding box over all polygons, this can speed up tree intersection/cheaply improve approximation for complex multi-polygons
// these are pre-encoded with LatLonPoint's encoding
final GeoRect box = Polygon.getBoundingBox(polygons);
final byte minLat[] = new byte[Integer.BYTES];
final byte maxLat[] = new byte[Integer.BYTES];
final byte minLon[] = new byte[Integer.BYTES];
final byte maxLon[] = new byte[Integer.BYTES];
NumericUtils.intToSortableBytes(LatLonPoint.encodeLatitude(box.minLat), minLat, 0);
NumericUtils.intToSortableBytes(LatLonPoint.encodeLatitude(box.maxLat), maxLat, 0);
NumericUtils.intToSortableBytes(LatLonPoint.encodeLongitude(box.minLon), minLon, 0);
NumericUtils.intToSortableBytes(LatLonPoint.encodeLongitude(box.maxLon), maxLon, 0);
// TODO: make this fancier, but currently linear with number of vertices
float cumulativeCost = 0;
for (Polygon polygon : polygons) {
cumulativeCost += 20 * (polygon.getPolyLats().length + polygon.getHoles().length);
}
final float matchCost = cumulativeCost;
return new ConstantScoreWeight(this) {
@ -120,27 +138,36 @@ final class LatLonPointInPolygonQuery extends Query {
@Override
public void visit(int docID, byte[] packedValue) {
// TODO: range checks
// we bounds check individual values, as subtrees may cross, but we are being sent the values anyway:
// this reduces the amount of docvalues fetches (improves approximation)
if (StringHelper.compare(Integer.BYTES, packedValue, 0, maxLat, 0) > 0 ||
StringHelper.compare(Integer.BYTES, packedValue, 0, minLat, 0) < 0 ||
StringHelper.compare(Integer.BYTES, packedValue, Integer.BYTES, maxLon, 0) > 0 ||
StringHelper.compare(Integer.BYTES, packedValue, Integer.BYTES, minLon, 0) < 0) {
// outside of global bounding box range
return;
}
result.add(docID);
}
@Override
public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) {
if (StringHelper.compare(Integer.BYTES, minPackedValue, 0, maxLat, 0) > 0 ||
StringHelper.compare(Integer.BYTES, maxPackedValue, 0, minLat, 0) < 0 ||
StringHelper.compare(Integer.BYTES, minPackedValue, Integer.BYTES, maxLon, 0) > 0 ||
StringHelper.compare(Integer.BYTES, maxPackedValue, Integer.BYTES, minLon, 0) < 0) {
// outside of global bounding box range
return Relation.CELL_OUTSIDE_QUERY;
}
double cellMinLat = LatLonPoint.decodeLatitude(minPackedValue, 0);
double cellMinLon = LatLonPoint.decodeLongitude(minPackedValue, Integer.BYTES);
double cellMaxLat = LatLonPoint.decodeLatitude(maxPackedValue, 0);
double cellMaxLon = LatLonPoint.decodeLongitude(maxPackedValue, Integer.BYTES);
if (cellMinLat <= minLat && cellMaxLat >= maxLat && cellMinLon <= minLon && cellMaxLon >= maxLon) {
// Cell fully encloses the query
return Relation.CELL_CROSSES_QUERY;
} else if (GeoRelationUtils.rectWithinPolyPrecise(cellMinLat, cellMaxLat, cellMinLon, cellMaxLon,
polyLats, polyLons,
minLat, maxLat, minLon, maxLon)) {
if (Polygon.contains(polygons, cellMinLat, cellMaxLat, cellMinLon, cellMaxLon)) {
return Relation.CELL_INSIDE_QUERY;
} else if (GeoRelationUtils.rectCrossesPolyPrecise(cellMinLat, cellMaxLat, cellMinLon, cellMaxLon,
polyLats, polyLons,
minLat, maxLat, minLon, maxLon)) {
} else if (Polygon.crosses(polygons, cellMinLat, cellMaxLat, cellMinLon, cellMaxLon)) {
return Relation.CELL_CROSSES_QUERY;
} else {
return Relation.CELL_OUTSIDE_QUERY;
@ -156,6 +183,7 @@ final class LatLonPointInPolygonQuery extends Query {
// return two-phase iterator using docvalues to postfilter candidates
SortedNumericDocValues docValues = DocValues.getSortedNumeric(reader, field);
TwoPhaseIterator iterator = new TwoPhaseIterator(disi) {
@Override
public boolean matches() throws IOException {
@ -169,7 +197,7 @@ final class LatLonPointInPolygonQuery extends Query {
long encoded = docValues.valueAt(i);
double docLatitude = LatLonPoint.decodeLatitude((int)(encoded >> 32));
double docLongitude = LatLonPoint.decodeLongitude((int)(encoded & 0xFFFFFFFF));
if (GeoRelationUtils.pointInPolygon(polyLats, polyLons, docLatitude, docLongitude)) {
if (Polygon.contains(polygons, docLatitude, docLongitude)) {
return true;
}
}
@ -179,7 +207,7 @@ final class LatLonPointInPolygonQuery extends Query {
@Override
public float matchCost() {
return 20 * polyLons.length; // TODO: make this fancier, but currently linear with number of vertices
return matchCost;
}
};
return new ConstantScoreScorer(this, score(), iterator);
@ -187,64 +215,36 @@ final class LatLonPointInPolygonQuery extends Query {
};
}
/** Returns the query field */
public String getField() {
return field;
}
public double getMinLat() {
return minLat;
}
public double getMaxLat() {
return maxLat;
}
public double getMinLon() {
return minLon;
}
public double getMaxLon() {
return maxLon;
}
public double[] getPolyLats() {
return polyLats;
}
public double[] getPolyLons() {
return polyLons;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
LatLonPointInPolygonQuery that = (LatLonPointInPolygonQuery) o;
if (field.equals(that.field) == false) {
return false;
}
if (Arrays.equals(polyLons, that.polyLons) == false) {
return false;
}
if (Arrays.equals(polyLats, that.polyLats) == false) {
return false;
}
return true;
/** Returns a copy of the internal polygon array */
public Polygon[] getPolygons() {
return polygons.clone();
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = 31 * result + field.hashCode();
result = 31 * result + Arrays.hashCode(polyLons);
result = 31 * result + Arrays.hashCode(polyLats);
result = prime * result + field.hashCode();
result = prime * result + Arrays.hashCode(polygons);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!super.equals(obj)) return false;
if (getClass() != obj.getClass()) return false;
LatLonPointInPolygonQuery other = (LatLonPointInPolygonQuery) obj;
if (!field.equals(other.field)) return false;
if (!Arrays.equals(polygons, other.polygons)) return false;
return true;
}
@Override
public String toString(String field) {
final StringBuilder sb = new StringBuilder();
@ -255,14 +255,7 @@ final class LatLonPointInPolygonQuery extends Query {
sb.append(this.field);
sb.append(':');
}
sb.append(" Points: ");
for (int i=0; i<polyLons.length; ++i) {
sb.append("[")
.append(polyLats[i])
.append(", ")
.append(polyLons[i])
.append("] ");
}
sb.append(Arrays.toString(polygons));
return sb.toString();
}
}

View File

@ -19,6 +19,7 @@ package org.apache.lucene.search;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.LatLonPoint;
import org.apache.lucene.spatial.util.BaseGeoPointTestCase;
import org.apache.lucene.spatial.util.Polygon;
public class TestLatLonPointQueries extends BaseGeoPointTestCase {
@ -38,8 +39,8 @@ public class TestLatLonPointQueries extends BaseGeoPointTestCase {
}
@Override
protected Query newPolygonQuery(String field, double[] lats, double[] lons) {
return LatLonPoint.newPolygonQuery(field, lats, lons);
protected Query newPolygonQuery(String field, Polygon... polygons) {
return LatLonPoint.newPolygonQuery(field, polygons);
}
@Override

View File

@ -20,10 +20,11 @@ import java.util.Arrays;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.Query;
import org.apache.lucene.spatial.geopoint.document.GeoPointField;
import org.apache.lucene.spatial.geopoint.document.GeoPointField.TermEncoding;
import org.apache.lucene.spatial.util.GeoEncodingUtils;
import org.apache.lucene.spatial.util.GeoRect;
import org.apache.lucene.spatial.util.GeoUtils;
import org.apache.lucene.spatial.util.Polygon;
/** Implements a simple point in polygon query on a GeoPoint field. This is based on
* {@code GeoPointInBBoxQueryImpl} and is implemented using a
@ -39,41 +40,55 @@ import org.apache.lucene.spatial.util.GeoUtils;
* 1. The polygon coordinates need to be in either clockwise or counter-clockwise order.
* 2. The polygon must not be self-crossing, otherwise the query may result in unexpected behavior
* 3. All latitude/longitude values must be in decimal degrees.
* 4. Complex computational geometry (e.g., dateline wrapping, polygon with holes) is not supported
* 4. Complex computational geometry (e.g., dateline wrapping) is not supported
* 5. For more advanced GeoSpatial indexing and query operations see spatial module
*
* @lucene.experimental
*/
public final class GeoPointInPolygonQuery extends GeoPointInBBoxQuery {
// polygon position arrays - this avoids the use of any objects or
// or geo library dependencies
/** array of y (latitude) values (in degrees) */
protected final double[] polyLats;
/** array of x (longitude) values (in degrees) */
protected final double[] polyLons;
/** array of polygons being queried */
final Polygon[] polygons;
/**
* Constructs a new GeoPolygonQuery that will match encoded {@link org.apache.lucene.spatial.geopoint.document.GeoPointField} terms
* that fall within or on the boundary of the polygons defined by the input parameters.
*/
public GeoPointInPolygonQuery(String field, Polygon... polygons) {
this(field, TermEncoding.PREFIX, polygons);
}
/**
* Constructs a new GeoPolygonQuery that will match encoded {@link org.apache.lucene.spatial.geopoint.document.GeoPointField} terms
* that fall within or on the boundary of the polygon defined by the input parameters.
* @deprecated Use {@link #GeoPointInPolygonQuery(String, Polygon[])}.
*/
@Deprecated
public GeoPointInPolygonQuery(final String field, final double[] polyLats, final double[] polyLons) {
this(field, TermEncoding.PREFIX, GeoUtils.polyToBBox(polyLats, polyLons), polyLats, polyLons);
this(field, TermEncoding.PREFIX, polyLats, polyLons);
}
/**
* Constructs a new GeoPolygonQuery that will match encoded {@link org.apache.lucene.spatial.geopoint.document.GeoPointField} terms
* that fall within or on the boundary of the polygon defined by the input parameters.
* @deprecated Use {@link #GeoPointInPolygonQuery(String, GeoPointField.TermEncoding, Polygon[])} instead.
*/
@Deprecated
public GeoPointInPolygonQuery(final String field, final TermEncoding termEncoding, final double[] polyLats, final double[] polyLons) {
this(field, termEncoding, GeoUtils.polyToBBox(polyLats, polyLons), polyLats, polyLons);
this(field, termEncoding, new Polygon(polyLats, polyLons));
}
/** Common constructor, used only internally. */
private GeoPointInPolygonQuery(final String field, TermEncoding termEncoding, GeoRect bbox, final double[] polyLats, final double[] polyLons) {
super(field, termEncoding, bbox.minLat, bbox.maxLat, bbox.minLon, bbox.maxLon);
GeoUtils.checkPolygon(polyLats, polyLons);
this.polyLons = polyLons;
this.polyLats = polyLats;
/**
* Constructs a new GeoPolygonQuery that will match encoded {@link org.apache.lucene.spatial.geopoint.document.GeoPointField} terms
* that fall within or on the boundary of the polygon defined by the input parameters.
*/
public GeoPointInPolygonQuery(String field, TermEncoding termEncoding, Polygon... polygons) {
this(field, termEncoding, Polygon.getBoundingBox(polygons), polygons);
}
// internal constructor
private GeoPointInPolygonQuery(String field, TermEncoding termEncoding, GeoRect boundingBox, Polygon... polygons) {
super(field, termEncoding, boundingBox.minLat, boundingBox.maxLat, boundingBox.minLon, boundingBox.maxLon);
this.polygons = polygons.clone();
}
/** throw exception if trying to change rewrite method */
@ -83,32 +98,26 @@ public final class GeoPointInPolygonQuery extends GeoPointInBBoxQuery {
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
GeoPointInPolygonQuery that = (GeoPointInPolygonQuery) o;
if (!Arrays.equals(polyLats, that.polyLats)) return false;
if (!Arrays.equals(polyLons, that.polyLons)) return false;
return true;
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + Arrays.hashCode(polygons);
return result;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + Arrays.hashCode(polyLats);
result = 31 * result + Arrays.hashCode(polyLons);
return result;
public boolean equals(Object obj) {
if (this == obj) return true;
if (!super.equals(obj)) return false;
if (getClass() != obj.getClass()) return false;
GeoPointInPolygonQuery other = (GeoPointInPolygonQuery) obj;
if (!Arrays.equals(polygons, other.polygons)) return false;
return true;
}
/** print out this polygon query */
@Override
public String toString(String field) {
assert polyLats.length == polyLons.length;
final StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(':');
@ -117,31 +126,16 @@ public final class GeoPointInPolygonQuery extends GeoPointInBBoxQuery {
sb.append(getField());
sb.append(':');
}
sb.append(" Points: ");
for (int i=0; i<polyLats.length; ++i) {
sb.append("[")
.append(polyLats[i])
.append(", ")
.append(polyLons[i])
.append("] ");
}
sb.append(" Polygon: ");
sb.append(Arrays.toString(polygons));
return sb.toString();
}
/**
* API utility method for returning the array of longitudinal values for this GeoPolygon
* The returned array is not a copy so do not change it!
* API utility method for returning copy of the polygon array
*/
public double[] getLons() {
return this.polyLons;
}
/**
* API utility method for returning the array of latitudinal values for this GeoPolygon
* The returned array is not a copy so do not change it!
*/
public double[] getLats() {
return this.polyLats;
public Polygon[] getPolygons() {
return polygons.clone();
}
}

View File

@ -16,9 +16,11 @@
*/
package org.apache.lucene.spatial.geopoint.search;
import java.util.Objects;
import org.apache.lucene.search.MultiTermQuery;
import org.apache.lucene.spatial.geopoint.document.GeoPointField.TermEncoding;
import org.apache.lucene.spatial.util.GeoRelationUtils;
import org.apache.lucene.spatial.util.Polygon;
/** Package private implementation for the public facing GeoPointInPolygonQuery delegate class.
*
@ -26,11 +28,13 @@ import org.apache.lucene.spatial.util.GeoRelationUtils;
*/
final class GeoPointInPolygonQueryImpl extends GeoPointInBBoxQueryImpl {
private final GeoPointInPolygonQuery polygonQuery;
private final Polygon[] polygons;
GeoPointInPolygonQueryImpl(final String field, final TermEncoding termEncoding, final GeoPointInPolygonQuery q,
final double minLat, final double maxLat, final double minLon, final double maxLon) {
super(field, termEncoding, minLat, maxLat, minLon, maxLon);
polygonQuery = q;
this.polygonQuery = Objects.requireNonNull(q);
this.polygons = Objects.requireNonNull(q.polygons);
}
@Override
@ -54,14 +58,12 @@ final class GeoPointInPolygonQueryImpl extends GeoPointInBBoxQueryImpl {
@Override
protected boolean cellCrosses(final double minLat, final double maxLat, final double minLon, final double maxLon) {
return GeoRelationUtils.rectCrossesPolyApprox(minLat, maxLat, minLon, maxLon, polygonQuery.polyLats, polygonQuery.polyLons,
polygonQuery.minLat, polygonQuery.maxLat, polygonQuery.minLon, polygonQuery.maxLon);
return Polygon.crosses(polygons, minLat, maxLat, minLon, maxLon);
}
@Override
protected boolean cellWithin(final double minLat, final double maxLat, final double minLon, final double maxLon) {
return GeoRelationUtils.rectWithinPolyApprox(minLat, maxLat, minLon, maxLon, polygonQuery.polyLats, polygonQuery.polyLons,
polygonQuery.minLat, polygonQuery.maxLat, polygonQuery.minLon, polygonQuery.maxLon);
return Polygon.contains(polygons, minLat, maxLat, minLon, maxLon);
}
@Override
@ -79,25 +81,25 @@ final class GeoPointInPolygonQueryImpl extends GeoPointInBBoxQueryImpl {
*/
@Override
protected boolean postFilter(final double lat, final double lon) {
return GeoRelationUtils.pointInPolygon(polygonQuery.polyLats, polygonQuery.polyLons, lat, lon);
return Polygon.contains(polygons, lat, lon);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
GeoPointInPolygonQueryImpl that = (GeoPointInPolygonQueryImpl) o;
return !(polygonQuery != null ? !polygonQuery.equals(that.polygonQuery) : that.polygonQuery != null);
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + polygonQuery.hashCode();
return result;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + (polygonQuery != null ? polygonQuery.hashCode() : 0);
return result;
public boolean equals(Object obj) {
if (this == obj) return true;
if (!super.equals(obj)) return false;
if (getClass() != obj.getClass()) return false;
GeoPointInPolygonQueryImpl other = (GeoPointInPolygonQueryImpl) obj;
if (!polygonQuery.equals(other.polygonQuery)) return false;
return true;
}
}

View File

@ -99,180 +99,4 @@ public class GeoRelationUtils {
final double bMinLat, final double bMaxLat, final double bMinLon, final double bMaxLon) {
return !((aMaxLon < bMinLon || aMinLon > bMaxLon || aMaxLat < bMinLat || aMinLat > bMaxLat));
}
/////////////////////////
// Polygon relations
/////////////////////////
/**
* Convenience method for accurately computing whether a rectangle crosses a poly
*/
public static boolean rectCrossesPolyPrecise(final double rMinLat, final double rMaxLat,
final double rMinLon, final double rMaxLon,
final double[] shapeLat, final double[] shapeLon,
final double sMinLat, final double sMaxLat,
final double sMinLon, final double sMaxLon) {
// short-circuit: if the bounding boxes are disjoint then the shape does not cross
if (rectDisjoint(rMinLat, rMaxLat, rMinLon, rMaxLon, sMinLat, sMaxLat, sMinLon, sMaxLon)) {
return false;
}
return rectCrossesPoly(rMinLat, rMaxLat, rMinLon, rMaxLon, shapeLat, shapeLon);
}
/**
* Compute whether a rectangle crosses a shape. (touching not allowed) Includes a flag for approximating the
* relation.
*/
public static boolean rectCrossesPolyApprox(final double rMinLat, final double rMaxLat,
final double rMinLon, final double rMaxLon,
final double[] shapeLat, final double[] shapeLon,
final double sMinLat, final double sMaxLat,
final double sMinLon, final double sMaxLon) {
// short-circuit: if the bounding boxes are disjoint then the shape does not cross
if (rectDisjoint(rMinLat, rMaxLat, rMinLon, rMaxLon, sMinLat, sMaxLat, sMinLon, sMaxLon)) {
return false;
}
final int polyLength = shapeLon.length-1;
for (short p=0; p<polyLength; ++p) {
if (lineCrossesRect(shapeLat[p], shapeLon[p], shapeLat[p+1], shapeLon[p+1], rMinLat, rMaxLat, rMinLon, rMaxLon) == true) {
return true;
}
}
return false;
}
/**
* Accurately compute (within restrictions of cartesian decimal degrees) whether a rectangle crosses a polygon
*/
private static boolean rectCrossesPoly(final double rMinLat, final double rMaxLat,
final double rMinLon, final double rMaxLon,
final double[] shapeLats, final double[] shapeLons) {
final double[][] bbox = new double[][] { {rMinLon, rMinLat}, {rMaxLon, rMinLat}, {rMaxLon, rMaxLat}, {rMinLon, rMaxLat}, {rMinLon, rMinLat} };
final int polyLength = shapeLons.length-1;
double d, s, t, a1, b1, c1, a2, b2, c2;
double x00, y00, x01, y01, x10, y10, x11, y11;
// computes the intersection point between each bbox edge and the polygon edge
for (short b=0; b<4; ++b) {
a1 = bbox[b+1][1]-bbox[b][1];
b1 = bbox[b][0]-bbox[b+1][0];
c1 = a1*bbox[b+1][0] + b1*bbox[b+1][1];
for (int p=0; p<polyLength; ++p) {
a2 = shapeLats[p+1]-shapeLats[p];
b2 = shapeLons[p]-shapeLons[p+1];
// compute determinant
d = a1*b2 - a2*b1;
if (d != 0) {
// lines are not parallel, check intersecting points
c2 = a2*shapeLons[p+1] + b2*shapeLats[p+1];
s = (1/d)*(b2*c1 - b1*c2);
t = (1/d)*(a1*c2 - a2*c1);
x00 = StrictMath.min(bbox[b][0], bbox[b+1][0]) - GeoEncodingUtils.TOLERANCE;
x01 = StrictMath.max(bbox[b][0], bbox[b+1][0]) + GeoEncodingUtils.TOLERANCE;
y00 = StrictMath.min(bbox[b][1], bbox[b+1][1]) - GeoEncodingUtils.TOLERANCE;
y01 = StrictMath.max(bbox[b][1], bbox[b+1][1]) + GeoEncodingUtils.TOLERANCE;
x10 = StrictMath.min(shapeLons[p], shapeLons[p+1]) - GeoEncodingUtils.TOLERANCE;
x11 = StrictMath.max(shapeLons[p], shapeLons[p+1]) + GeoEncodingUtils.TOLERANCE;
y10 = StrictMath.min(shapeLats[p], shapeLats[p+1]) - GeoEncodingUtils.TOLERANCE;
y11 = StrictMath.max(shapeLats[p], shapeLats[p+1]) + GeoEncodingUtils.TOLERANCE;
// check whether the intersection point is touching one of the line segments
boolean touching = ((x00 == s && y00 == t) || (x01 == s && y01 == t))
|| ((x10 == s && y10 == t) || (x11 == s && y11 == t));
// if line segments are not touching and the intersection point is within the range of either segment
if (!(touching || x00 > s || x01 < s || y00 > t || y01 < t || x10 > s || x11 < s || y10 > t || y11 < t)) {
return true;
}
}
} // for each poly edge
} // for each bbox edge
return false;
}
private static boolean lineCrossesRect(double aLat1, double aLon1,
double aLat2, double aLon2,
final double rMinLat, final double rMaxLat,
final double rMinLon, final double rMaxLon) {
// short-circuit: if one point inside rect, other outside
if (pointInRectPrecise(aLat1, aLon1, rMinLat, rMaxLat, rMinLon, rMaxLon)) {
if (pointInRectPrecise(aLat2, aLon2, rMinLat, rMaxLat, rMinLon, rMaxLon) == false) {
return true;
}
} else if (pointInRectPrecise(aLat2, aLon2, rMinLat, rMaxLat, rMinLon, rMaxLon)) {
return true;
}
return lineCrossesLine(aLat1, aLon1, aLat2, aLon2, rMinLat, rMinLon, rMaxLat, rMaxLon)
|| lineCrossesLine(aLat1, aLon1, aLat2, aLon2, rMaxLat, rMinLon, rMinLat, rMaxLon);
}
private static boolean lineCrossesLine(final double aLat1, final double aLon1, final double aLat2, final double aLon2,
final double bLat1, final double bLon1, final double bLat2, final double bLon2) {
// determine if three points are ccw (right-hand rule) by computing the determinate
final double aX2X1d = aLon2 - aLon1;
final double aY2Y1d = aLat2 - aLat1;
final double bX2X1d = bLon2 - bLon1;
final double bY2Y1d = bLat2 - bLat1;
final double t1B = aX2X1d * (bLat2 - aLat1) - aY2Y1d * (bLon2 - aLon1);
final double test1 = (aX2X1d * (bLat1 - aLat1) - aY2Y1d * (bLon1 - aLon1)) * t1B;
final double t2B = bX2X1d * (aLat2 - bLat1) - bY2Y1d * (aLon2 - bLon1);
final double test2 = (bX2X1d * (aLat1 - bLat1) - bY2Y1d * (aLon1 - bLon1)) * t2B;
if (test1 < 0 && test2 < 0) {
return true;
}
if (test1 == 0 || test2 == 0) {
// vertically collinear
if (aLon1 == aLon2 || bLon1 == bLon2) {
final double minAy = Math.min(aLat1, aLat2);
final double maxAy = Math.max(aLat1, aLat2);
final double minBy = Math.min(bLat1, bLat2);
final double maxBy = Math.max(bLat1, bLat2);
return !(minBy >= maxAy || maxBy <= minAy);
}
// horizontally collinear
final double minAx = Math.min(aLon1, aLon2);
final double maxAx = Math.max(aLon1, aLon2);
final double minBx = Math.min(bLon1, bLon2);
final double maxBx = Math.max(bLon1, bLon2);
return !(minBx >= maxAx || maxBx <= minAx);
}
return false;
}
/**
* Computes whether a rectangle is within a polygon (shared boundaries not allowed) with more rigor than the
* {@link GeoRelationUtils#rectWithinPolyApprox} counterpart
*/
public static boolean rectWithinPolyPrecise(final double rMinLat, final double rMaxLat, final double rMinLon, final double rMaxLon,
final double[] shapeLats, final double[] shapeLons, final double sMinLat,
final double sMaxLat, final double sMinLon, final double sMaxLon) {
// check if rectangle crosses poly (to handle concave/pacman polys), then check that all 4 corners
// are contained
return !(rectCrossesPolyPrecise(rMinLat, rMaxLat, rMinLon, rMaxLon, shapeLats, shapeLons, sMinLat, sMaxLat, sMinLon, sMaxLon) ||
!pointInPolygon(shapeLats, shapeLons, rMinLat, rMinLon) || !pointInPolygon(shapeLats, shapeLons, rMinLat, rMaxLon) ||
!pointInPolygon(shapeLats, shapeLons, rMaxLat, rMaxLon) || !pointInPolygon(shapeLats, shapeLons, rMaxLat, rMinLon));
}
/**
* Computes whether a rectangle is within a given polygon (shared boundaries allowed)
*/
public static boolean rectWithinPolyApprox(final double rMinLat, final double rMaxLat, final double rMinLon, final double rMaxLon,
final double[] shapeLats, final double[] shapeLons, final double sMinLat,
final double sMaxLat, final double sMinLon, final double sMaxLon) {
// approximation: check if rectangle crosses poly (to handle concave/pacman polys), then check one of the corners
// are contained
// short-cut: if bounding boxes cross, rect is not within
if (rectCrosses(rMinLat, rMaxLat, rMinLon, rMaxLon, sMinLat, sMaxLat, sMinLon, sMaxLon) == true) {
return false;
}
return !(rectCrossesPolyApprox(rMinLat, rMaxLat, rMinLon, rMaxLon, shapeLats, shapeLons, sMinLat, sMaxLat, sMinLon, sMaxLon)
|| !pointInPolygon(shapeLats, shapeLons, rMinLat, rMinLon));
}
}

View File

@ -74,37 +74,6 @@ public final class GeoUtils {
}
}
/** validates polygon values are within standard +/-180 coordinate bounds, same
* number of latitude and longitude, and is closed
*/
public static void checkPolygon(double[] polyLats, double[] polyLons) {
if (polyLats == null) {
throw new IllegalArgumentException("polyLats must not be null");
}
if (polyLons == null) {
throw new IllegalArgumentException("polyLons must not be null");
}
if (polyLats.length != polyLons.length) {
throw new IllegalArgumentException("polyLats and polyLons must be equal length");
}
if (polyLats.length != polyLons.length) {
throw new IllegalArgumentException("polyLats and polyLons must be equal length");
}
if (polyLats.length < 4) {
throw new IllegalArgumentException("at least 4 polygon points required");
}
if (polyLats[0] != polyLats[polyLats.length-1]) {
throw new IllegalArgumentException("first and last points of the polygon must be the same (it must close itself): polyLats[0]=" + polyLats[0] + " polyLats[" + (polyLats.length-1) + "]=" + polyLats[polyLats.length-1]);
}
if (polyLons[0] != polyLons[polyLons.length-1]) {
throw new IllegalArgumentException("first and last points of the polygon must be the same (it must close itself): polyLons[0]=" + polyLons[0] + " polyLons[" + (polyLons.length-1) + "]=" + polyLons[polyLons.length-1]);
}
for (int i = 0; i < polyLats.length; i++) {
checkLatitude(polyLats[i]);
checkLongitude(polyLons[i]);
}
}
/** Compute Bounding Box for a circle using WGS-84 parameters */
public static GeoRect circleToBBox(final double centerLat, final double centerLon, final double radiusMeters) {
final double radLat = TO_RADIANS * centerLat;
@ -136,24 +105,6 @@ public final class GeoUtils {
return new GeoRect(TO_DEGREES * minLat, TO_DEGREES * maxLat, TO_DEGREES * minLon, TO_DEGREES * maxLon);
}
/** Compute Bounding Box for a polygon using WGS-84 parameters */
public static GeoRect polyToBBox(double[] polyLats, double[] polyLons) {
checkPolygon(polyLats, polyLons);
double minLon = Double.POSITIVE_INFINITY;
double maxLon = Double.NEGATIVE_INFINITY;
double minLat = Double.POSITIVE_INFINITY;
double maxLat = Double.NEGATIVE_INFINITY;
for (int i=0;i<polyLats.length;i++) {
minLat = min(polyLats[i], minLat);
maxLat = max(polyLats[i], maxLat);
minLon = min(polyLons[i], minLon);
maxLon = max(polyLons[i], maxLon);
}
return new GeoRect(minLat, maxLat, minLon, maxLon);
}
// some sloppyish stuff, do we really need this to be done in a sloppy way?
// unless it is performance sensitive, we should try to remove.

View File

@ -0,0 +1,330 @@
/*
* 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.spatial.util;
import java.util.Arrays;
/**
* Represents a closed polygon on the earth's surface.
* @lucene.experimental
*/
public final class Polygon {
private final double[] polyLats;
private final double[] polyLons;
private final Polygon[] holes;
/** minimum latitude of this polygon's bounding box area */
public final double minLat;
/** maximum latitude of this polygon's bounding box area */
public final double maxLat;
/** minimum longitude of this polygon's bounding box area */
public final double minLon;
/** maximum longitude of this polygon's bounding box area */
public final double maxLon;
// TODO: we could also compute the maximal inner bounding box, to make relations faster to compute?
/**
* Creates a new Polygon from the supplied latitude/longitude array, and optionally any holes.
*/
public Polygon(double[] polyLats, double[] polyLons, Polygon... holes) {
if (polyLats == null) {
throw new IllegalArgumentException("polyLats must not be null");
}
if (polyLons == null) {
throw new IllegalArgumentException("polyLons must not be null");
}
if (holes == null) {
throw new IllegalArgumentException("holes must not be null");
}
if (polyLats.length != polyLons.length) {
throw new IllegalArgumentException("polyLats and polyLons must be equal length");
}
if (polyLats.length != polyLons.length) {
throw new IllegalArgumentException("polyLats and polyLons must be equal length");
}
if (polyLats.length < 4) {
throw new IllegalArgumentException("at least 4 polygon points required");
}
if (polyLats[0] != polyLats[polyLats.length-1]) {
throw new IllegalArgumentException("first and last points of the polygon must be the same (it must close itself): polyLats[0]=" + polyLats[0] + " polyLats[" + (polyLats.length-1) + "]=" + polyLats[polyLats.length-1]);
}
if (polyLons[0] != polyLons[polyLons.length-1]) {
throw new IllegalArgumentException("first and last points of the polygon must be the same (it must close itself): polyLons[0]=" + polyLons[0] + " polyLons[" + (polyLons.length-1) + "]=" + polyLons[polyLons.length-1]);
}
for (int i = 0; i < polyLats.length; i++) {
GeoUtils.checkLatitude(polyLats[i]);
GeoUtils.checkLongitude(polyLons[i]);
}
for (int i = 0; i < holes.length; i++) {
Polygon inner = holes[i];
if (inner.holes.length > 0) {
throw new IllegalArgumentException("holes may not contain holes: polygons may not nest.");
}
}
this.polyLats = polyLats.clone();
this.polyLons = polyLons.clone();
this.holes = holes.clone();
// compute bounding box
double minLat = Double.POSITIVE_INFINITY;
double maxLat = Double.NEGATIVE_INFINITY;
double minLon = Double.POSITIVE_INFINITY;
double maxLon = Double.NEGATIVE_INFINITY;
for (int i = 0;i < polyLats.length; i++) {
minLat = Math.min(polyLats[i], minLat);
maxLat = Math.max(polyLats[i], maxLat);
minLon = Math.min(polyLons[i], minLon);
maxLon = Math.max(polyLons[i], maxLon);
}
this.minLat = minLat;
this.maxLat = maxLat;
this.minLon = minLon;
this.maxLon = maxLon;
}
/** Returns true if the point is contained within this polygon */
public boolean contains(double latitude, double longitude) {
// check bounding box
if (latitude < minLat || latitude > maxLat || longitude < minLon || longitude > maxLon) {
return false;
}
/*
* simple even-odd point in polygon computation
* 1. Determine if point is contained in the longitudinal range
* 2. Determine whether point crosses the edge by computing the latitudinal delta
* between the end-point of a parallel vector (originating at the point) and the
* y-component of the edge sink
*
* NOTE: Requires polygon point (x,y) order either clockwise or counter-clockwise
*/
boolean inPoly = false;
/*
* Note: This is using a euclidean coordinate system which could result in
* upwards of 110KM error at the equator.
* TODO convert coordinates to cylindrical projection (e.g. mercator)
*/
for (int i = 1; i < polyLats.length; i++) {
if (polyLons[i] <= longitude && polyLons[i-1] >= longitude || polyLons[i-1] <= longitude && polyLons[i] >= longitude) {
if (polyLats[i] + (longitude - polyLons[i]) / (polyLons[i-1] - polyLons[i]) * (polyLats[i-1] - polyLats[i]) <= latitude) {
inPoly = !inPoly;
}
}
}
if (inPoly) {
for (Polygon hole : holes) {
if (hole.contains(latitude, longitude)) {
return false;
}
}
return true;
} else {
return false;
}
}
/**
* Computes whether a rectangle is within a polygon (shared boundaries not allowed)
*/
public boolean contains(double minLat, double maxLat, double minLon, double maxLon) {
// check if rectangle crosses poly (to handle concave/pacman polys), then check that all 4 corners
// are contained
boolean contains = crosses(minLat, maxLat, minLon, maxLon) == false &&
contains(minLat, minLon) &&
contains(minLat, maxLon) &&
contains(maxLat, maxLon) &&
contains(maxLat, minLon);
if (contains) {
// if we intersect with any hole, game over
for (Polygon hole : holes) {
if (hole.crosses(minLat, maxLat, minLon, maxLon) || hole.contains(minLat, maxLat, minLon, maxLon)) {
return false;
}
}
return true;
} else {
return false;
}
}
/**
* Convenience method for accurately computing whether a rectangle crosses a poly.
*/
public boolean crosses(double minLat, double maxLat, final double minLon, final double maxLon) {
// if the bounding boxes are disjoint then the shape does not cross
if (maxLon < this.minLon || minLon > this.maxLon || maxLat < this.minLat || minLat > this.maxLat) {
return false;
}
// if the rectangle fully encloses us, we cross.
if (minLat <= this.minLat && maxLat >= this.maxLat && minLon <= this.minLon && maxLon >= this.maxLon) {
return true;
}
// if we cross any hole, we cross
for (Polygon hole : holes) {
if (hole.crosses(minLat, maxLat, minLon, maxLon)) {
return true;
}
}
/*
* Accurately compute (within restrictions of cartesian decimal degrees) whether a rectangle crosses a polygon
*/
final double[][] bbox = new double[][] { {minLon, minLat}, {maxLon, minLat}, {maxLon, maxLat}, {minLon, maxLat}, {minLon, minLat} };
final int polyLength = polyLons.length-1;
double d, s, t, a1, b1, c1, a2, b2, c2;
double x00, y00, x01, y01, x10, y10, x11, y11;
// computes the intersection point between each bbox edge and the polygon edge
for (short b=0; b<4; ++b) {
a1 = bbox[b+1][1]-bbox[b][1];
b1 = bbox[b][0]-bbox[b+1][0];
c1 = a1*bbox[b+1][0] + b1*bbox[b+1][1];
for (int p=0; p<polyLength; ++p) {
a2 = polyLats[p+1]-polyLats[p];
b2 = polyLons[p]-polyLons[p+1];
// compute determinant
d = a1*b2 - a2*b1;
if (d != 0) {
// lines are not parallel, check intersecting points
c2 = a2*polyLons[p+1] + b2*polyLats[p+1];
s = (1/d)*(b2*c1 - b1*c2);
t = (1/d)*(a1*c2 - a2*c1);
x00 = Math.min(bbox[b][0], bbox[b+1][0]) - GeoEncodingUtils.TOLERANCE;
x01 = Math.max(bbox[b][0], bbox[b+1][0]) + GeoEncodingUtils.TOLERANCE;
y00 = Math.min(bbox[b][1], bbox[b+1][1]) - GeoEncodingUtils.TOLERANCE;
y01 = Math.max(bbox[b][1], bbox[b+1][1]) + GeoEncodingUtils.TOLERANCE;
x10 = Math.min(polyLons[p], polyLons[p+1]) - GeoEncodingUtils.TOLERANCE;
x11 = Math.max(polyLons[p], polyLons[p+1]) + GeoEncodingUtils.TOLERANCE;
y10 = Math.min(polyLats[p], polyLats[p+1]) - GeoEncodingUtils.TOLERANCE;
y11 = Math.max(polyLats[p], polyLats[p+1]) + GeoEncodingUtils.TOLERANCE;
// check whether the intersection point is touching one of the line segments
boolean touching = ((x00 == s && y00 == t) || (x01 == s && y01 == t))
|| ((x10 == s && y10 == t) || (x11 == s && y11 == t));
// if line segments are not touching and the intersection point is within the range of either segment
if (!(touching || x00 > s || x01 < s || y00 > t || y01 < t || x10 > s || x11 < s || y10 > t || y11 < t)) {
return true;
}
}
} // for each poly edge
} // for each bbox edge
return false;
}
/** Returns a copy of the internal latitude array */
public double[] getPolyLats() {
return polyLats.clone();
}
/** Returns a copy of the internal longitude array */
public double[] getPolyLons() {
return polyLons.clone();
}
/** Returns a copy of the internal holes array */
public Polygon[] getHoles() {
return holes.clone();
}
/** Returns the bounding box over an array of polygons */
public static GeoRect getBoundingBox(Polygon[] polygons) {
// compute bounding box
double minLat = Double.POSITIVE_INFINITY;
double maxLat = Double.NEGATIVE_INFINITY;
double minLon = Double.POSITIVE_INFINITY;
double maxLon = Double.NEGATIVE_INFINITY;
for (int i = 0;i < polygons.length; i++) {
minLat = Math.min(polygons[i].minLat, minLat);
maxLat = Math.max(polygons[i].maxLat, maxLat);
minLon = Math.min(polygons[i].minLon, minLon);
maxLon = Math.max(polygons[i].maxLon, maxLon);
}
return new GeoRect(minLat, maxLat, minLon, maxLon);
}
/** Helper for multipolygon logic: returns true if any of the supplied polygons contain the point */
public static boolean contains(Polygon[] polygons, double latitude, double longitude) {
for (Polygon polygon : polygons) {
if (polygon.contains(latitude, longitude)) {
return true;
}
}
return false;
}
/** Helper for multipolygon logic: returns true if any of the supplied polygons contain the rectangle */
public static boolean contains(Polygon[] polygons, double minLat, double maxLat, double minLon, double maxLon) {
for (Polygon polygon : polygons) {
if (polygon.contains(minLat, maxLat, minLon, maxLon)) {
return true;
}
}
return false;
}
/** Helper for multipolygon logic: returns true if any of the supplied polygons crosses the rectangle */
public static boolean crosses(Polygon[] polygons, double minLat, double maxLat, double minLon, double maxLon) {
for (Polygon polygon : polygons) {
if (polygon.crosses(minLat, maxLat, minLon, maxLon)) {
return true;
}
}
return false;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(holes);
result = prime * result + Arrays.hashCode(polyLats);
result = prime * result + Arrays.hashCode(polyLons);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
Polygon other = (Polygon) obj;
if (!Arrays.equals(holes, other.holes)) return false;
if (!Arrays.equals(polyLats, other.polyLats)) return false;
if (!Arrays.equals(polyLons, other.polyLons)) return false;
return true;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < polyLats.length; i++) {
sb.append("[")
.append(polyLats[i])
.append(", ")
.append(polyLons[i])
.append("] ");
}
if (holes.length > 0) {
sb.append(", holes=");
sb.append(Arrays.toString(holes));
}
return sb.toString();
}
}

View File

@ -19,6 +19,7 @@ package org.apache.lucene.spatial.geopoint.search;
import org.apache.lucene.document.Document;
import org.apache.lucene.search.Query;
import org.apache.lucene.spatial.util.GeoEncodingUtils;
import org.apache.lucene.spatial.util.Polygon;
import org.apache.lucene.spatial.geopoint.document.GeoPointField;
import org.apache.lucene.spatial.geopoint.document.GeoPointField.TermEncoding;
import org.apache.lucene.spatial.util.BaseGeoPointTestCase;
@ -56,8 +57,8 @@ public class TestGeoPointQuery extends BaseGeoPointTestCase {
}
@Override
protected Query newPolygonQuery(String field, double[] lats, double[] lons) {
return new GeoPointInPolygonQuery(field, TermEncoding.PREFIX, lats, lons);
protected Query newPolygonQuery(String field, Polygon... polygons) {
return new GeoPointInPolygonQuery(field, TermEncoding.PREFIX, polygons);
}
}

View File

@ -19,6 +19,7 @@ package org.apache.lucene.spatial.geopoint.search;
import org.apache.lucene.document.Document;
import org.apache.lucene.search.Query;
import org.apache.lucene.spatial.util.GeoEncodingUtils;
import org.apache.lucene.spatial.util.Polygon;
import org.apache.lucene.spatial.geopoint.document.GeoPointField;
import org.apache.lucene.spatial.geopoint.document.GeoPointField.TermEncoding;
import org.apache.lucene.spatial.util.BaseGeoPointTestCase;
@ -56,8 +57,8 @@ public class TestLegacyGeoPointQuery extends BaseGeoPointTestCase {
}
@Override
protected Query newPolygonQuery(String field, double[] lats, double[] lons) {
return new GeoPointInPolygonQuery(field, TermEncoding.NUMERIC, lats, lons);
protected Query newPolygonQuery(String field, Polygon... polygons) {
return new GeoPointInPolygonQuery(field, TermEncoding.NUMERIC, polygons);
}
// legacy encoding is too slow somehow for this random test, its not up to the task.
@ -75,4 +76,6 @@ public class TestLegacyGeoPointQuery extends BaseGeoPointTestCase {
public void testSamePointManyTimes() throws Exception {
assumeTrue("legacy encoding goes OOM on this test", false);
}
}

View File

@ -285,9 +285,81 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
// search and verify we found our doc
IndexReader reader = writer.getReader();
IndexSearcher searcher = newSearcher(reader);
assertEquals(1, searcher.count(newPolygonQuery("field",
assertEquals(1, searcher.count(newPolygonQuery("field", new Polygon(
new double[] { 18, 18, 19, 19, 18 },
new double[] { -66, -65, -65, -66, -66 })));
new double[] { -66, -65, -65, -66, -66 }))));
reader.close();
writer.close();
dir.close();
}
/** test we can search for a polygon with a hole (but still includes the doc) */
public void testPolygonHole() throws Exception {
Directory dir = newDirectory();
RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
// add a doc with a point
Document document = new Document();
addPointToDoc("field", document, 18.313694, -65.227444);
writer.addDocument(document);
// search and verify we found our doc
IndexReader reader = writer.getReader();
IndexSearcher searcher = newSearcher(reader);
Polygon inner = new Polygon(new double[] { 18.5, 18.5, 18.7, 18.7, 18.5 },
new double[] { -65.7, -65.4, -65.4, -65.7, -65.7 });
Polygon outer = new Polygon(new double[] { 18, 18, 19, 19, 18 },
new double[] { -66, -65, -65, -66, -66 }, inner);
assertEquals(1, searcher.count(newPolygonQuery("field", outer)));
reader.close();
writer.close();
dir.close();
}
/** test we can search for a polygon with a hole (that excludes the doc) */
public void testPolygonHoleExcludes() throws Exception {
Directory dir = newDirectory();
RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
// add a doc with a point
Document document = new Document();
addPointToDoc("field", document, 18.313694, -65.227444);
writer.addDocument(document);
// search and verify we found our doc
IndexReader reader = writer.getReader();
IndexSearcher searcher = newSearcher(reader);
Polygon inner = new Polygon(new double[] { 18.2, 18.2, 18.4, 18.4, 18.2 },
new double[] { -65.3, -65.2, -65.2, -65.3, -65.3 });
Polygon outer = new Polygon(new double[] { 18, 18, 19, 19, 18 },
new double[] { -66, -65, -65, -66, -66 }, inner);
assertEquals(0, searcher.count(newPolygonQuery("field", outer)));
reader.close();
writer.close();
dir.close();
}
/** test we can search for a multi-polygon */
public void testMultiPolygonBasics() throws Exception {
Directory dir = newDirectory();
RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
// add a doc with a point
Document document = new Document();
addPointToDoc("field", document, 18.313694, -65.227444);
writer.addDocument(document);
// search and verify we found our doc
IndexReader reader = writer.getReader();
IndexSearcher searcher = newSearcher(reader);
Polygon a = new Polygon(new double[] { 28, 28, 29, 29, 28 },
new double[] { -56, -55, -55, -56, -56 });
Polygon b = new Polygon(new double[] { 18, 18, 19, 19, 18 },
new double[] { -66, -65, -65, -66, -66 });
assertEquals(1, searcher.count(newPolygonQuery("field", a, b)));
reader.close();
writer.close();
@ -297,62 +369,12 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
/** null field name not allowed */
public void testPolygonNullField() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
newPolygonQuery(null,
newPolygonQuery(null, new Polygon(
new double[] { 18, 18, 19, 19, 18 },
new double[] { -66, -65, -65, -66, -66 });
new double[] { -66, -65, -65, -66, -66 }));
});
assertTrue(expected.getMessage().contains("field must not be null"));
}
/** null polyLats not allowed */
public void testPolygonNullPolyLats() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
newPolygonQuery("test",
null,
new double[] { -66, -65, -65, -66, -66 });
});
assertTrue(expected.getMessage().contains("polyLats must not be null"));
}
/** null polyLons not allowed */
public void testPolygonNullPolyLons() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
newPolygonQuery("test",
new double[] { 18, 18, 19, 19, 18 },
null);
});
assertTrue(expected.getMessage().contains("polyLons must not be null"));
}
/** polygon needs at least 3 vertices */
public void testPolygonLine() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
newPolygonQuery("test",
new double[] { 18, 18, 18 },
new double[] { -66, -65, -66 });
});
assertTrue(expected.getMessage().contains("at least 4 polygon points required"));
}
/** polygon needs same number of latitudes as longitudes */
public void testPolygonBogus() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
newPolygonQuery("test",
new double[] { 18, 18, 19, 19 },
new double[] { -66, -65, -65, -66, -66 });
});
assertTrue(expected.getMessage().contains("must be equal length"));
}
/** polygon must be closed */
public void testPolygonNotClosed() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
newPolygonQuery("test",
new double[] { 18, 18, 19, 19, 19 },
new double[] { -66, -65, -65, -66, -67 });
});
assertTrue(expected.getMessage(), expected.getMessage().contains("it must close itself"));
}
// A particularly tricky adversary for BKD tree:
public void testSamePointManyTimes() throws Exception {
@ -710,7 +732,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
protected abstract Query newDistanceQuery(String field, double centerLat, double centerLon, double radiusMeters);
protected abstract Query newPolygonQuery(String field, double[] lats, double[] lons);
protected abstract Query newPolygonQuery(String field, Polygon... polygon);
static final boolean rectContainsPoint(GeoRect rect, double pointLat, double pointLon) {
assert Double.isNaN(pointLat) == false;
@ -1072,16 +1094,14 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
}
// Polygon
final double[][] polygon;
final Polygon polygon;
if (small) {
polygon = GeoTestUtil.nextPolygonNear(originLat, originLon);
} else {
polygon = GeoTestUtil.nextPolygon();
}
final double[] polyLats = polygon[0];
final double[] polyLons = polygon[1];
Query query = newPolygonQuery(FIELD_NAME, polyLats, polyLons);
Query query = newPolygonQuery(FIELD_NAME, polygon);
if (VERBOSE) {
System.out.println(" query=" + query);
@ -1118,7 +1138,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
} else if (Double.isNaN(lats[id])) {
expected = false;
} else {
expected = GeoRelationUtils.pointInPolygon(polyLats, polyLons, lats[id], lons[id]);
expected = polygon.contains(lats[id], lons[id]);
}
if (hits.get(docID) != expected) {
@ -1132,8 +1152,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
b.append(" query=" + query + " docID=" + docID + "\n");
b.append(" lat=" + lats[id] + " lon=" + lons[id] + "\n");
b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false));
b.append(" polyLats=" + Arrays.toString(polyLats));
b.append(" polyLons=" + Arrays.toString(polyLons));
b.append(" polygon=" + polygon);
if (true) {
fail("wrong hit (first of possibly more):\n\n" + b);
} else {
@ -1317,10 +1336,10 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
lons[3] = rect.maxLon;
lats[4] = rect.minLat;
lons[4] = rect.minLon;
q1 = newPolygonQuery("field", lats, lons);
q2 = newPolygonQuery("field", lats, lons);
q1 = newPolygonQuery("field", new Polygon(lats, lons));
q2 = newPolygonQuery("field", new Polygon(lats, lons));
assertEquals(q1, q2);
assertFalse(q1.equals(newPolygonQuery("field2", lats, lons)));
assertFalse(q1.equals(newPolygonQuery("field2", new Polygon(lats, lons))));
}
/** return topdocs over a small set of points in field "point" */
@ -1406,18 +1425,20 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase {
public void testSmallSetPoly() throws Exception {
TopDocs td = searchSmallSet(newPolygonQuery("point",
new Polygon(
new double[]{33.073130, 32.9942669, 32.938386, 33.0374494,
33.1369762, 33.1162747, 33.073130, 33.073130},
new double[]{-96.7682647, -96.8280029, -96.6288757, -96.4929199,
-96.6041564, -96.7449188, -96.76826477, -96.7682647}),
-96.6041564, -96.7449188, -96.76826477, -96.7682647})),
5);
assertEquals(2, td.totalHits);
}
public void testSmallSetPolyWholeMap() throws Exception {
TopDocs td = searchSmallSet(newPolygonQuery("point",
new Polygon(
new double[] {GeoUtils.MIN_LAT_INCL, GeoUtils.MAX_LAT_INCL, GeoUtils.MAX_LAT_INCL, GeoUtils.MIN_LAT_INCL, GeoUtils.MIN_LAT_INCL},
new double[] {GeoUtils.MIN_LON_INCL, GeoUtils.MIN_LON_INCL, GeoUtils.MAX_LON_INCL, GeoUtils.MAX_LON_INCL, GeoUtils.MIN_LON_INCL}),
new double[] {GeoUtils.MIN_LON_INCL, GeoUtils.MIN_LON_INCL, GeoUtils.MAX_LON_INCL, GeoUtils.MAX_LON_INCL, GeoUtils.MIN_LON_INCL})),
20);
assertEquals("testWholeMap failed", 24, td.totalHits);
}

View File

@ -85,7 +85,7 @@ public class GeoTestUtil {
}
/** returns next pseudorandom polygon */
public static double[][] nextPolygon() {
public static Polygon nextPolygon() {
if (random().nextBoolean()) {
return surpriseMePolygon(null, null);
}
@ -101,7 +101,7 @@ public class GeoTestUtil {
}
/** returns next pseudorandom polygon, kinda close to {@code otherLatitude} and {@code otherLongitude} */
public static double[][] nextPolygonNear(double otherLatitude, double otherLongitude) {
public static Polygon nextPolygonNear(double otherLatitude, double otherLongitude) {
if (random().nextBoolean()) {
return surpriseMePolygon(otherLatitude, otherLongitude);
}
@ -133,7 +133,7 @@ public class GeoTestUtil {
return new GeoRect(lat0, lat1, lon0, lon1);
}
private static double[][] boxPolygon(GeoRect box) {
private static Polygon boxPolygon(GeoRect box) {
assert box.crossesDateline() == false;
final double[] polyLats = new double[5];
final double[] polyLons = new double[5];
@ -147,10 +147,10 @@ public class GeoTestUtil {
polyLons[3] = box.maxLon;
polyLats[4] = box.minLat;
polyLons[4] = box.minLon;
return new double[][] { polyLats, polyLons };
return new Polygon(polyLats, polyLons);
}
private static double[][] trianglePolygon(GeoRect box) {
private static Polygon trianglePolygon(GeoRect box) {
assert box.crossesDateline() == false;
final double[] polyLats = new double[4];
final double[] polyLons = new double[4];
@ -162,11 +162,10 @@ public class GeoTestUtil {
polyLons[2] = box.maxLon;
polyLats[3] = box.minLat;
polyLons[3] = box.minLon;
return new double[][] { polyLats, polyLons };
return new Polygon(polyLats, polyLons);
}
/** Returns {polyLats, polyLons} double[] array */
private static double[][] surpriseMePolygon(Double otherLatitude, Double otherLongitude) {
private static Polygon surpriseMePolygon(Double otherLatitude, Double otherLongitude) {
// repeat until we get a poly that doesn't cross dateline:
newPoly:
while (true) {
@ -232,7 +231,7 @@ public class GeoTestUtil {
latsArray[i] = lats.get(i);
lonsArray[i] = lons.get(i);
}
return new double[][] {latsArray, lonsArray};
return new Polygon(latsArray, lonsArray);
}
}

View File

@ -264,65 +264,6 @@ public class TestGeoUtils extends LuceneTestCase {
}
}
public void testPacManPolyQuery() throws Exception {
// pacman
double[] px = {0, 10, 10, 0, -8, -10, -8, 0, 10, 10, 0};
double[] py = {0, 5, 9, 10, 9, 0, -9, -10, -9, -5, 0};
// shape bbox
double xMinA = -10;
double xMaxA = 10;
double yMinA = -10;
double yMaxA = 10;
// candidate crosses cell
double xMin = 2;//-5;
double xMax = 11;//0.000001;
double yMin = -1;//0;
double yMax = 1;//5;
// test cell crossing poly
assertTrue(GeoRelationUtils.rectCrossesPolyApprox(yMin, yMax, xMin, yMax, py, px, yMinA, yMaxA, xMinA, xMaxA));
assertFalse(GeoRelationUtils.rectCrossesPolyApprox(0, 5, -5, 0.000001, py, px, yMin, yMax, xMin, xMax));
assertTrue(GeoRelationUtils.rectWithinPolyApprox(0, 5, -5, -2, py, px, yMin, yMax, xMin, xMax));
}
public void testPolyToBBox() throws Exception {
for (int i = 0; i < 1000; i++) {
double[][] polygon = GeoTestUtil.nextPolygon();
GeoRect box = GeoUtils.polyToBBox(polygon[0], polygon[1]);
assertFalse(box.crossesDateline());
for (int j = 0; j < 1000; j++) {
double latitude = GeoTestUtil.nextLatitude();
double longitude = GeoTestUtil.nextLongitude();
// if the point is within poly, then it should be in our bounding box
if (GeoRelationUtils.pointInPolygon(polygon[0], polygon[1], latitude, longitude)) {
assertTrue(latitude >= box.minLat && latitude <= box.maxLat);
assertTrue(longitude >= box.minLon && longitude <= box.maxLon);
}
}
}
}
public void testPolyToBBoxEdgeCases() throws Exception {
for (int i = 0; i < 1000; i++) {
double[][] polygon = GeoTestUtil.nextPolygon();
GeoRect box = GeoUtils.polyToBBox(polygon[0], polygon[1]);
assertFalse(box.crossesDateline());
for (int j = 0; j < 1000; j++) {
double latitude = GeoTestUtil.nextLatitudeAround(box.minLat, box.maxLat);
double longitude = GeoTestUtil.nextLongitudeAround(box.minLon, box.maxLon);
// if the point is within poly, then it should be in our bounding box
if (GeoRelationUtils.pointInPolygon(polygon[0], polygon[1], latitude, longitude)) {
assertTrue(latitude >= box.minLat && latitude <= box.maxLat);
assertTrue(longitude >= box.minLon && longitude <= box.maxLon);
}
}
}
}
public void testAxisLat() {
double earthCircumference = 2D * Math.PI * GeoUtils.SEMIMAJOR_AXIS;
assertEquals(90, GeoUtils.axisLat(0, earthCircumference / 4), 0.0D);

View File

@ -0,0 +1,141 @@
/*
* 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.spatial.util;
import org.apache.lucene.util.LuceneTestCase;
public class TestPolygon extends LuceneTestCase {
/** null polyLats not allowed */
public void testPolygonNullPolyLats() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
new Polygon(null, new double[] { -66, -65, -65, -66, -66 });
});
assertTrue(expected.getMessage().contains("polyLats must not be null"));
}
/** null polyLons not allowed */
public void testPolygonNullPolyLons() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
new Polygon(new double[] { 18, 18, 19, 19, 18 }, null);
});
assertTrue(expected.getMessage().contains("polyLons must not be null"));
}
/** polygon needs at least 3 vertices */
public void testPolygonLine() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
new Polygon(new double[] { 18, 18, 18 }, new double[] { -66, -65, -66 });
});
assertTrue(expected.getMessage().contains("at least 4 polygon points required"));
}
/** polygon needs same number of latitudes as longitudes */
public void testPolygonBogus() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
new Polygon(new double[] { 18, 18, 19, 19 }, new double[] { -66, -65, -65, -66, -66 });
});
assertTrue(expected.getMessage().contains("must be equal length"));
}
/** polygon must be closed */
public void testPolygonNotClosed() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
new Polygon(new double[] { 18, 18, 19, 19, 19 }, new double[] { -66, -65, -65, -66, -67 });
});
assertTrue(expected.getMessage(), expected.getMessage().contains("it must close itself"));
}
/** Three boxes, an island inside a hole inside a shape */
public void testMultiPolygon() {
Polygon hole = new Polygon(new double[] { -10, -10, 10, 10, -10 }, new double[] { -10, 10, 10, -10, -10 });
Polygon outer = new Polygon(new double[] { -50, -50, 50, 50, -50 }, new double[] { -50, 50, 50, -50, -50 }, hole);
Polygon island = new Polygon(new double[] { -5, -5, 5, 5, -5 }, new double[] { -5, 5, 5, -5, -5 } );
Polygon polygons[] = new Polygon[] { outer, island };
// contains(point)
assertTrue(Polygon.contains(polygons, -2, 2)); // on the island
assertFalse(Polygon.contains(polygons, -6, 6)); // in the hole
assertTrue(Polygon.contains(polygons, -25, 25)); // on the mainland
assertFalse(Polygon.contains(polygons, -51, 51)); // in the ocean
// contains(box): this can conservatively return false
assertTrue(Polygon.contains(polygons, -2, 2, -2, 2)); // on the island
assertFalse(Polygon.contains(polygons, 6, 7, 6, 7)); // in the hole
assertTrue(Polygon.contains(polygons, 24, 25, 24, 25)); // on the mainland
assertFalse(Polygon.contains(polygons, 51, 52, 51, 52)); // in the ocean
assertFalse(Polygon.contains(polygons, -60, 60, -60, 60)); // enclosing us completely
assertFalse(Polygon.contains(polygons, 49, 51, 49, 51)); // overlapping the mainland
assertFalse(Polygon.contains(polygons, 9, 11, 9, 11)); // overlapping the hole
assertFalse(Polygon.contains(polygons, 5, 6, 5, 6)); // overlapping the island
// crosses(box): this can conservatively return true
assertTrue(Polygon.crosses(polygons, -60, 60, -60, 60)); // enclosing us completely
assertTrue(Polygon.crosses(polygons, 49, 51, 49, 51)); // overlapping the mainland and ocean
assertTrue(Polygon.crosses(polygons, 9, 11, 9, 11)); // overlapping the hole and mainland
assertTrue(Polygon.crosses(polygons, 5, 6, 5, 6)); // overlapping the island
}
public void testPacMan() throws Exception {
// pacman
double[] px = {0, 10, 10, 0, -8, -10, -8, 0, 10, 10, 0};
double[] py = {0, 5, 9, 10, 9, 0, -9, -10, -9, -5, 0};
// candidate crosses cell
double xMin = 2;//-5;
double xMax = 11;//0.000001;
double yMin = -1;//0;
double yMax = 1;//5;
// test cell crossing poly
Polygon polygon = new Polygon(py, px);
assertTrue(polygon.crosses(yMin, yMax, xMin, xMax));
assertFalse(polygon.contains(yMin, yMax, xMin, xMax));
}
public void testBoundingBox() throws Exception {
for (int i = 0; i < 100; i++) {
Polygon polygon = GeoTestUtil.nextPolygon();
for (int j = 0; j < 100; j++) {
double latitude = GeoTestUtil.nextLatitude();
double longitude = GeoTestUtil.nextLongitude();
// if the point is within poly, then it should be in our bounding box
if (polygon.contains(latitude, longitude)) {
assertTrue(latitude >= polygon.minLat && latitude <= polygon.maxLat);
assertTrue(longitude >= polygon.minLon && longitude <= polygon.maxLon);
}
}
}
}
public void testBoundingBoxEdgeCases() throws Exception {
for (int i = 0; i < 100; i++) {
Polygon polygon = GeoTestUtil.nextPolygon();
for (int j = 0; j < 100; j++) {
double latitude = GeoTestUtil.nextLatitudeAround(polygon.minLat, polygon.maxLat);
double longitude = GeoTestUtil.nextLongitudeAround(polygon.minLon, polygon.maxLon);
// if the point is within poly, then it should be in our bounding box
if (polygon.contains(latitude, longitude)) {
assertTrue(latitude >= polygon.minLat && latitude <= polygon.maxLat);
assertTrue(longitude >= polygon.minLon && longitude <= polygon.maxLon);
}
}
}
}
}