blob: 35061bb5534e8cb14ee8f94bd89dfb2cf0f5e593 [file] [log] [blame]
diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPoint.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPoint.java
index 60e8300..6e9e715 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPoint.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPoint.java
@@ -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);
}
/**
diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointInPolygonQuery.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointInPolygonQuery.java
index 454a3dd..2875e2f 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointInPolygonQuery.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonPointInPolygonQuery.java
@@ -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,65 +215,37 @@ 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();
sb.append(getClass().getSimpleName());
@@ -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();
}
}
diff --git a/lucene/sandbox/src/test/org/apache/lucene/search/TestLatLonPointQueries.java b/lucene/sandbox/src/test/org/apache/lucene/search/TestLatLonPointQueries.java
index 3f4da8a..3bde389 100644
--- a/lucene/sandbox/src/test/org/apache/lucene/search/TestLatLonPointQueries.java
+++ b/lucene/sandbox/src/test/org/apache/lucene/search/TestLatLonPointQueries.java
@@ -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
diff --git a/lucene/spatial/src/java/org/apache/lucene/spatial/geopoint/search/GeoPointInPolygonQuery.java b/lucene/spatial/src/java/org/apache/lucene/spatial/geopoint/search/GeoPointInPolygonQuery.java
index e75030c..d0a3149 100644
--- a/lucene/spatial/src/java/org/apache/lucene/spatial/geopoint/search/GeoPointInPolygonQuery.java
+++ b/lucene/spatial/src/java/org/apache/lucene/spatial/geopoint/search/GeoPointInPolygonQuery.java
@@ -23,7 +23,7 @@ import org.apache.lucene.search.Query;
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 +39,54 @@ 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;
+ protected 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, 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 +96,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;
- }
-
- @Override
public int hashCode() {
+ final int prime = 31;
int result = super.hashCode();
- result = 31 * result + Arrays.hashCode(polyLats);
- result = 31 * result + Arrays.hashCode(polyLons);
+ 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;
+ 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 +124,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!
- */
- 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!
+ * API utility method for returning copy of the polygon array
*/
- public double[] getLats() {
- return this.polyLats;
+ public Polygon[] getPolygons() {
+ return polygons.clone();
}
}
diff --git a/lucene/spatial/src/java/org/apache/lucene/spatial/geopoint/search/GeoPointInPolygonQueryImpl.java b/lucene/spatial/src/java/org/apache/lucene/spatial/geopoint/search/GeoPointInPolygonQueryImpl.java
index acace24..d499a47 100644
--- a/lucene/spatial/src/java/org/apache/lucene/spatial/geopoint/search/GeoPointInPolygonQueryImpl.java
+++ b/lucene/spatial/src/java/org/apache/lucene/spatial/geopoint/search/GeoPointInPolygonQueryImpl.java
@@ -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);
- }
-
- @Override
public int hashCode() {
+ final int prime = 31;
int result = super.hashCode();
- result = 31 * result + (polygonQuery != null ? polygonQuery.hashCode() : 0);
+ result = prime * result + polygonQuery.hashCode();
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;
+ GeoPointInPolygonQueryImpl other = (GeoPointInPolygonQueryImpl) obj;
+ if (!polygonQuery.equals(other.polygonQuery)) return false;
+ return true;
+ }
}
diff --git a/lucene/spatial/src/java/org/apache/lucene/spatial/util/GeoRelationUtils.java b/lucene/spatial/src/java/org/apache/lucene/spatial/util/GeoRelationUtils.java
index ba94e9d..50f9446 100644
--- a/lucene/spatial/src/java/org/apache/lucene/spatial/util/GeoRelationUtils.java
+++ b/lucene/spatial/src/java/org/apache/lucene/spatial/util/GeoRelationUtils.java
@@ -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));
- }
}
diff --git a/lucene/spatial/src/java/org/apache/lucene/spatial/util/GeoUtils.java b/lucene/spatial/src/java/org/apache/lucene/spatial/util/GeoUtils.java
index 52e9405..6d7f615 100644
--- a/lucene/spatial/src/java/org/apache/lucene/spatial/util/GeoUtils.java
+++ b/lucene/spatial/src/java/org/apache/lucene/spatial/util/GeoUtils.java
@@ -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.
diff --git a/lucene/spatial/src/java/org/apache/lucene/spatial/util/Polygon.java b/lucene/spatial/src/java/org/apache/lucene/spatial/util/Polygon.java
new file mode 100644
index 0000000..5dca23d
--- /dev/null
+++ b/lucene/spatial/src/java/org/apache/lucene/spatial/util/Polygon.java
@@ -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();
+ }
+}
diff --git a/lucene/spatial/src/test/org/apache/lucene/spatial/geopoint/search/TestGeoPointQuery.java b/lucene/spatial/src/test/org/apache/lucene/spatial/geopoint/search/TestGeoPointQuery.java
index 0f0eaeb..91afe3f 100644
--- a/lucene/spatial/src/test/org/apache/lucene/spatial/geopoint/search/TestGeoPointQuery.java
+++ b/lucene/spatial/src/test/org/apache/lucene/spatial/geopoint/search/TestGeoPointQuery.java
@@ -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);
}
}
diff --git a/lucene/spatial/src/test/org/apache/lucene/spatial/geopoint/search/TestLegacyGeoPointQuery.java b/lucene/spatial/src/test/org/apache/lucene/spatial/geopoint/search/TestLegacyGeoPointQuery.java
index 00cc279..cd15f66 100644
--- a/lucene/spatial/src/test/org/apache/lucene/spatial/geopoint/search/TestLegacyGeoPointQuery.java
+++ b/lucene/spatial/src/test/org/apache/lucene/spatial/geopoint/search/TestLegacyGeoPointQuery.java
@@ -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);
}
+
+
}
diff --git a/lucene/spatial/src/test/org/apache/lucene/spatial/util/BaseGeoPointTestCase.java b/lucene/spatial/src/test/org/apache/lucene/spatial/util/BaseGeoPointTestCase.java
index dd0e09b..1b18a18 100644
--- a/lucene/spatial/src/test/org/apache/lucene/spatial/util/BaseGeoPointTestCase.java
+++ b/lucene/spatial/src/test/org/apache/lucene/spatial/util/BaseGeoPointTestCase.java
@@ -285,73 +285,95 @@ 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();
}
- /** null field name not allowed */
- public void testPolygonNullField() {
- IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
- newPolygonQuery(null,
- new double[] { 18, 18, 19, 19, 18 },
- 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"));
+ /** 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();
}
- /** 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"));
+ /** 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();
}
- /** 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"));
+ /** 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();
+ dir.close();
}
- /** polygon must be closed */
- public void testPolygonNotClosed() {
+ /** null field name not allowed */
+ public void testPolygonNullField() {
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
- newPolygonQuery("test",
- new double[] { 18, 18, 19, 19, 19 },
- new double[] { -66, -65, -65, -66, -67 });
+ newPolygonQuery(null, new Polygon(
+ new double[] { 18, 18, 19, 19, 18 },
+ new double[] { -66, -65, -65, -66, -66 }));
});
- assertTrue(expected.getMessage(), expected.getMessage().contains("it must close itself"));
+ assertTrue(expected.getMessage().contains("field must not be null"));
}
// A particularly tricky adversary for BKD tree:
@@ -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);
}
diff --git a/lucene/spatial/src/test/org/apache/lucene/spatial/util/GeoTestUtil.java b/lucene/spatial/src/test/org/apache/lucene/spatial/util/GeoTestUtil.java
index e52c8a5..1785c12 100644
--- a/lucene/spatial/src/test/org/apache/lucene/spatial/util/GeoTestUtil.java
+++ b/lucene/spatial/src/test/org/apache/lucene/spatial/util/GeoTestUtil.java
@@ -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);
}
}
diff --git a/lucene/spatial/src/test/org/apache/lucene/spatial/util/TestGeoUtils.java b/lucene/spatial/src/test/org/apache/lucene/spatial/util/TestGeoUtils.java
index 9d6549a..f3b9ad1 100644
--- a/lucene/spatial/src/test/org/apache/lucene/spatial/util/TestGeoUtils.java
+++ b/lucene/spatial/src/test/org/apache/lucene/spatial/util/TestGeoUtils.java
@@ -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);
diff --git a/lucene/spatial/src/test/org/apache/lucene/spatial/util/TestPolygon.java b/lucene/spatial/src/test/org/apache/lucene/spatial/util/TestPolygon.java
new file mode 100644
index 0000000..4ccd8d4
--- /dev/null
+++ b/lucene/spatial/src/test/org/apache/lucene/spatial/util/TestPolygon.java
@@ -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);
+ }
+ }
+ }
+ }
+}