diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 7b7c9d38daa..636b6422791 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -43,6 +43,10 @@ Optimizations * LUCENE-7147: Improve disjoint check for geo distance query traversal (Ryan Ernst, Robert Muir, Mike McCandless) +* LUCENE-7153: GeoPointField and LatLonPoint polygon queries now support + multiple polygons and holes, with memory usage independent of + polygon complexity. (Karl Wright, Mike McCandless, Robert Muir) + Bug Fixes * LUCENE-7127: Fix corner case bugs in GeoPointDistanceQuery. (Robert Muir) 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 60e83000083..6e9e7154be5 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. *

- * 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 454a3ddf68b..2875e2f0423 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. * *

The field must be indexed with using {@link org.apache.lucene.document.LatLonPoint} added per document. * @@ -52,29 +53,27 @@ import org.apache.lucene.spatial.util.GeoUtils; final class LatLonPointInPolygonQuery extends Query { final String field; - final double minLat; - final double maxLat; - final double minLon; - final double maxLon; - final double[] polyLats; - final double[] polyLons; + final Polygon[] polygons; /** The lats/lons must be clockwise or counter-clockwise. */ - public LatLonPointInPolygonQuery(String field, double[] polyLats, double[] polyLons) { + public LatLonPointInPolygonQuery(String field, Polygon[] polygons) { if (field == null) { throw new IllegalArgumentException("field must not be null"); } - GeoUtils.checkPolygon(polyLats, polyLons); + if (polygons == null) { + throw new IllegalArgumentException("polygons must not be null"); + } + if (polygons.length == 0) { + throw new IllegalArgumentException("polygons must not be empty"); + } + for (int i = 0; i < polygons.length; i++) { + if (polygons[i] == null) { + throw new IllegalArgumentException("polygon[" + i + "] must not be null"); + } + } this.field = field; - this.polyLats = polyLats; - this.polyLons = polyLons; - + this.polygons = polygons.clone(); // TODO: we could also compute the maximal inner bounding box, to make relations faster to compute? - GeoRect box = GeoUtils.polyToBBox(polyLats, polyLons); - this.minLon = box.minLon; - this.maxLon = box.maxLon; - this.minLat = box.minLat; - this.maxLat = box.maxLat; } @Override @@ -82,6 +81,25 @@ final class LatLonPointInPolygonQuery extends Query { // I don't use RandomAccessWeight here: it's no good to approximate with "match all docs"; this is an inverted structure and should be // used in the first pass: + + // bounding box over all polygons, this can speed up tree intersection/cheaply improve approximation for complex multi-polygons + // these are pre-encoded with LatLonPoint's encoding + final GeoRect box = Polygon.getBoundingBox(polygons); + final byte minLat[] = new byte[Integer.BYTES]; + final byte maxLat[] = new byte[Integer.BYTES]; + final byte minLon[] = new byte[Integer.BYTES]; + final byte maxLon[] = new byte[Integer.BYTES]; + NumericUtils.intToSortableBytes(LatLonPoint.encodeLatitude(box.minLat), minLat, 0); + NumericUtils.intToSortableBytes(LatLonPoint.encodeLatitude(box.maxLat), maxLat, 0); + NumericUtils.intToSortableBytes(LatLonPoint.encodeLongitude(box.minLon), minLon, 0); + NumericUtils.intToSortableBytes(LatLonPoint.encodeLongitude(box.maxLon), maxLon, 0); + + // TODO: make this fancier, but currently linear with number of vertices + float cumulativeCost = 0; + for (Polygon polygon : polygons) { + cumulativeCost += 20 * (polygon.getPolyLats().length + polygon.getHoles().length); + } + final float matchCost = cumulativeCost; return new ConstantScoreWeight(this) { @@ -120,27 +138,36 @@ final class LatLonPointInPolygonQuery extends Query { @Override public void visit(int docID, byte[] packedValue) { - // TODO: range checks + // we bounds check individual values, as subtrees may cross, but we are being sent the values anyway: + // this reduces the amount of docvalues fetches (improves approximation) + if (StringHelper.compare(Integer.BYTES, packedValue, 0, maxLat, 0) > 0 || + StringHelper.compare(Integer.BYTES, packedValue, 0, minLat, 0) < 0 || + StringHelper.compare(Integer.BYTES, packedValue, Integer.BYTES, maxLon, 0) > 0 || + StringHelper.compare(Integer.BYTES, packedValue, Integer.BYTES, minLon, 0) < 0) { + // outside of global bounding box range + return; + } result.add(docID); } @Override public Relation compare(byte[] minPackedValue, byte[] maxPackedValue) { + if (StringHelper.compare(Integer.BYTES, minPackedValue, 0, maxLat, 0) > 0 || + StringHelper.compare(Integer.BYTES, maxPackedValue, 0, minLat, 0) < 0 || + StringHelper.compare(Integer.BYTES, minPackedValue, Integer.BYTES, maxLon, 0) > 0 || + StringHelper.compare(Integer.BYTES, maxPackedValue, Integer.BYTES, minLon, 0) < 0) { + // outside of global bounding box range + return Relation.CELL_OUTSIDE_QUERY; + } + double cellMinLat = LatLonPoint.decodeLatitude(minPackedValue, 0); double cellMinLon = LatLonPoint.decodeLongitude(minPackedValue, Integer.BYTES); double cellMaxLat = LatLonPoint.decodeLatitude(maxPackedValue, 0); double cellMaxLon = LatLonPoint.decodeLongitude(maxPackedValue, Integer.BYTES); - if (cellMinLat <= minLat && cellMaxLat >= maxLat && cellMinLon <= minLon && cellMaxLon >= maxLon) { - // Cell fully encloses the query - return Relation.CELL_CROSSES_QUERY; - } else if (GeoRelationUtils.rectWithinPolyPrecise(cellMinLat, cellMaxLat, cellMinLon, cellMaxLon, - polyLats, polyLons, - minLat, maxLat, minLon, maxLon)) { + if (Polygon.contains(polygons, cellMinLat, cellMaxLat, cellMinLon, cellMaxLon)) { return Relation.CELL_INSIDE_QUERY; - } else if (GeoRelationUtils.rectCrossesPolyPrecise(cellMinLat, cellMaxLat, cellMinLon, cellMaxLon, - polyLats, polyLons, - minLat, maxLat, minLon, maxLon)) { + } else if (Polygon.crosses(polygons, cellMinLat, cellMaxLat, cellMinLon, cellMaxLon)) { return Relation.CELL_CROSSES_QUERY; } else { return Relation.CELL_OUTSIDE_QUERY; @@ -156,6 +183,7 @@ final class LatLonPointInPolygonQuery extends Query { // return two-phase iterator using docvalues to postfilter candidates SortedNumericDocValues docValues = DocValues.getSortedNumeric(reader, field); + TwoPhaseIterator iterator = new TwoPhaseIterator(disi) { @Override public boolean matches() throws IOException { @@ -169,7 +197,7 @@ final class LatLonPointInPolygonQuery extends Query { long encoded = docValues.valueAt(i); double docLatitude = LatLonPoint.decodeLatitude((int)(encoded >> 32)); double docLongitude = LatLonPoint.decodeLongitude((int)(encoded & 0xFFFFFFFF)); - if (GeoRelationUtils.pointInPolygon(polyLats, polyLons, docLatitude, docLongitude)) { + if (Polygon.contains(polygons, docLatitude, docLongitude)) { return true; } } @@ -179,7 +207,7 @@ final class LatLonPointInPolygonQuery extends Query { @Override public float matchCost() { - return 20 * polyLons.length; // TODO: make this fancier, but currently linear with number of vertices + return matchCost; } }; return new ConstantScoreScorer(this, score(), iterator); @@ -187,64 +215,36 @@ final class LatLonPointInPolygonQuery extends Query { }; } + /** Returns the query field */ public String getField() { return field; } - public double getMinLat() { - return minLat; - } - - public double getMaxLat() { - return maxLat; - } - - public double getMinLon() { - return minLon; - } - - public double getMaxLon() { - return maxLon; - } - - public double[] getPolyLats() { - return polyLats; - } - - public double[] getPolyLons() { - return polyLons; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - - LatLonPointInPolygonQuery that = (LatLonPointInPolygonQuery) o; - - if (field.equals(that.field) == false) { - return false; - } - if (Arrays.equals(polyLons, that.polyLons) == false) { - return false; - } - if (Arrays.equals(polyLats, that.polyLats) == false) { - return false; - } - - return true; + /** Returns a copy of the internal polygon array */ + public Polygon[] getPolygons() { + return polygons.clone(); } @Override public int hashCode() { + final int prime = 31; int result = super.hashCode(); - result = 31 * result + field.hashCode(); - result = 31 * result + Arrays.hashCode(polyLons); - result = 31 * result + Arrays.hashCode(polyLats); + result = prime * result + field.hashCode(); + result = prime * result + Arrays.hashCode(polygons); return result; } + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!super.equals(obj)) return false; + if (getClass() != obj.getClass()) return false; + LatLonPointInPolygonQuery other = (LatLonPointInPolygonQuery) obj; + if (!field.equals(other.field)) return false; + if (!Arrays.equals(polygons, other.polygons)) return false; + return true; + } + @Override public String toString(String field) { final StringBuilder sb = new StringBuilder(); @@ -255,14 +255,7 @@ final class LatLonPointInPolygonQuery extends Query { sb.append(this.field); sb.append(':'); } - sb.append(" Points: "); - for (int i=0; i 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 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 52e94052798..6d7f615ba2f 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 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 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 0f0eaeb555c..91afe3fd7e2 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 00cc279a00a..cd15f6691a5 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 dd0e09be96d..1b18a18fbc5 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,9 +285,81 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { // search and verify we found our doc IndexReader reader = writer.getReader(); IndexSearcher searcher = newSearcher(reader); - assertEquals(1, searcher.count(newPolygonQuery("field", + assertEquals(1, searcher.count(newPolygonQuery("field", new Polygon( new double[] { 18, 18, 19, 19, 18 }, - new double[] { -66, -65, -65, -66, -66 }))); + new double[] { -66, -65, -65, -66, -66 })))); + + reader.close(); + writer.close(); + dir.close(); + } + + /** test we can search for a polygon with a hole (but still includes the doc) */ + public void testPolygonHole() throws Exception { + Directory dir = newDirectory(); + RandomIndexWriter writer = new RandomIndexWriter(random(), dir); + + // add a doc with a point + Document document = new Document(); + addPointToDoc("field", document, 18.313694, -65.227444); + writer.addDocument(document); + + // search and verify we found our doc + IndexReader reader = writer.getReader(); + IndexSearcher searcher = newSearcher(reader); + Polygon inner = new Polygon(new double[] { 18.5, 18.5, 18.7, 18.7, 18.5 }, + new double[] { -65.7, -65.4, -65.4, -65.7, -65.7 }); + Polygon outer = new Polygon(new double[] { 18, 18, 19, 19, 18 }, + new double[] { -66, -65, -65, -66, -66 }, inner); + assertEquals(1, searcher.count(newPolygonQuery("field", outer))); + + reader.close(); + writer.close(); + dir.close(); + } + + /** test we can search for a polygon with a hole (that excludes the doc) */ + public void testPolygonHoleExcludes() throws Exception { + Directory dir = newDirectory(); + RandomIndexWriter writer = new RandomIndexWriter(random(), dir); + + // add a doc with a point + Document document = new Document(); + addPointToDoc("field", document, 18.313694, -65.227444); + writer.addDocument(document); + + // search and verify we found our doc + IndexReader reader = writer.getReader(); + IndexSearcher searcher = newSearcher(reader); + Polygon inner = new Polygon(new double[] { 18.2, 18.2, 18.4, 18.4, 18.2 }, + new double[] { -65.3, -65.2, -65.2, -65.3, -65.3 }); + Polygon outer = new Polygon(new double[] { 18, 18, 19, 19, 18 }, + new double[] { -66, -65, -65, -66, -66 }, inner); + assertEquals(0, searcher.count(newPolygonQuery("field", outer))); + + reader.close(); + writer.close(); + dir.close(); + } + + /** test we can search for a multi-polygon */ + public void testMultiPolygonBasics() throws Exception { + Directory dir = newDirectory(); + RandomIndexWriter writer = new RandomIndexWriter(random(), dir); + + // add a doc with a point + Document document = new Document(); + addPointToDoc("field", document, 18.313694, -65.227444); + writer.addDocument(document); + + // search and verify we found our doc + IndexReader reader = writer.getReader(); + IndexSearcher searcher = newSearcher(reader); + Polygon a = new Polygon(new double[] { 28, 28, 29, 29, 28 }, + new double[] { -56, -55, -55, -56, -56 }); + Polygon b = new Polygon(new double[] { 18, 18, 19, 19, 18 }, + new double[] { -66, -65, -65, -66, -66 }); + assertEquals(1, searcher.count(newPolygonQuery("field", a, b))); reader.close(); writer.close(); @@ -297,62 +369,12 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { /** null field name not allowed */ public void testPolygonNullField() { IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { - newPolygonQuery(null, + newPolygonQuery(null, new Polygon( new double[] { 18, 18, 19, 19, 18 }, - new double[] { -66, -65, -65, -66, -66 }); + new double[] { -66, -65, -65, -66, -66 })); }); assertTrue(expected.getMessage().contains("field must not be null")); } - - /** null polyLats not allowed */ - public void testPolygonNullPolyLats() { - IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { - newPolygonQuery("test", - null, - new double[] { -66, -65, -65, -66, -66 }); - }); - assertTrue(expected.getMessage().contains("polyLats must not be null")); - } - - /** null polyLons not allowed */ - public void testPolygonNullPolyLons() { - IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { - newPolygonQuery("test", - new double[] { 18, 18, 19, 19, 18 }, - null); - }); - assertTrue(expected.getMessage().contains("polyLons must not be null")); - } - - /** polygon needs at least 3 vertices */ - public void testPolygonLine() { - IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { - newPolygonQuery("test", - new double[] { 18, 18, 18 }, - new double[] { -66, -65, -66 }); - }); - assertTrue(expected.getMessage().contains("at least 4 polygon points required")); - } - - /** polygon needs same number of latitudes as longitudes */ - public void testPolygonBogus() { - IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { - newPolygonQuery("test", - new double[] { 18, 18, 19, 19 }, - new double[] { -66, -65, -65, -66, -66 }); - }); - assertTrue(expected.getMessage().contains("must be equal length")); - } - - /** polygon must be closed */ - public void testPolygonNotClosed() { - IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> { - newPolygonQuery("test", - new double[] { 18, 18, 19, 19, 19 }, - new double[] { -66, -65, -65, -66, -67 }); - }); - assertTrue(expected.getMessage(), expected.getMessage().contains("it must close itself")); - } // A particularly tricky adversary for BKD tree: public void testSamePointManyTimes() throws Exception { @@ -710,7 +732,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { protected abstract Query newDistanceQuery(String field, double centerLat, double centerLon, double radiusMeters); - protected abstract Query newPolygonQuery(String field, double[] lats, double[] lons); + protected abstract Query newPolygonQuery(String field, Polygon... polygon); static final boolean rectContainsPoint(GeoRect rect, double pointLat, double pointLon) { assert Double.isNaN(pointLat) == false; @@ -1072,16 +1094,14 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { } // Polygon - final double[][] polygon; + final Polygon polygon; if (small) { polygon = GeoTestUtil.nextPolygonNear(originLat, originLon); } else { polygon = GeoTestUtil.nextPolygon(); } - final double[] polyLats = polygon[0]; - final double[] polyLons = polygon[1]; - Query query = newPolygonQuery(FIELD_NAME, polyLats, polyLons); + Query query = newPolygonQuery(FIELD_NAME, polygon); if (VERBOSE) { System.out.println(" query=" + query); @@ -1118,7 +1138,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { } else if (Double.isNaN(lats[id])) { expected = false; } else { - expected = GeoRelationUtils.pointInPolygon(polyLats, polyLons, lats[id], lons[id]); + expected = polygon.contains(lats[id], lons[id]); } if (hits.get(docID) != expected) { @@ -1132,8 +1152,7 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { b.append(" query=" + query + " docID=" + docID + "\n"); b.append(" lat=" + lats[id] + " lon=" + lons[id] + "\n"); b.append(" deleted?=" + (liveDocs != null && liveDocs.get(docID) == false)); - b.append(" polyLats=" + Arrays.toString(polyLats)); - b.append(" polyLons=" + Arrays.toString(polyLons)); + b.append(" polygon=" + polygon); if (true) { fail("wrong hit (first of possibly more):\n\n" + b); } else { @@ -1317,10 +1336,10 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { lons[3] = rect.maxLon; lats[4] = rect.minLat; lons[4] = rect.minLon; - q1 = newPolygonQuery("field", lats, lons); - q2 = newPolygonQuery("field", lats, lons); + q1 = newPolygonQuery("field", new Polygon(lats, lons)); + q2 = newPolygonQuery("field", new Polygon(lats, lons)); assertEquals(q1, q2); - assertFalse(q1.equals(newPolygonQuery("field2", lats, lons))); + assertFalse(q1.equals(newPolygonQuery("field2", new Polygon(lats, lons)))); } /** return topdocs over a small set of points in field "point" */ @@ -1406,18 +1425,20 @@ public abstract class BaseGeoPointTestCase extends LuceneTestCase { public void testSmallSetPoly() throws Exception { TopDocs td = searchSmallSet(newPolygonQuery("point", + new Polygon( new double[]{33.073130, 32.9942669, 32.938386, 33.0374494, 33.1369762, 33.1162747, 33.073130, 33.073130}, new double[]{-96.7682647, -96.8280029, -96.6288757, -96.4929199, - -96.6041564, -96.7449188, -96.76826477, -96.7682647}), + -96.6041564, -96.7449188, -96.76826477, -96.7682647})), 5); assertEquals(2, td.totalHits); } public void testSmallSetPolyWholeMap() throws Exception { TopDocs td = searchSmallSet(newPolygonQuery("point", + new Polygon( new double[] {GeoUtils.MIN_LAT_INCL, GeoUtils.MAX_LAT_INCL, GeoUtils.MAX_LAT_INCL, GeoUtils.MIN_LAT_INCL, GeoUtils.MIN_LAT_INCL}, - new double[] {GeoUtils.MIN_LON_INCL, GeoUtils.MIN_LON_INCL, GeoUtils.MAX_LON_INCL, GeoUtils.MAX_LON_INCL, GeoUtils.MIN_LON_INCL}), + new double[] {GeoUtils.MIN_LON_INCL, GeoUtils.MIN_LON_INCL, GeoUtils.MAX_LON_INCL, GeoUtils.MAX_LON_INCL, GeoUtils.MIN_LON_INCL})), 20); assertEquals("testWholeMap failed", 24, td.totalHits); } 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 e52c8a587d9..1785c12f445 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 9d6549a010c..f3b9ad1a21c 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 00000000000..4ccd8d423df --- /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); + } + } + } + } +}