mirror of https://github.com/apache/lucene.git
LUCENE-7153: give GeoPointField and LatLonPoint full polygon support
This commit is contained in:
parent
2af578e167
commit
81c83b4431
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue