| 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); |
| + } |
| + } |
| + } |
| + } |
| +} |