diff --git a/lucene/core/src/java/org/apache/lucene/geo/Polygon.java b/lucene/core/src/java/org/apache/lucene/geo/Polygon.java index 361c199fc31..3b5dec9ca2d 100644 --- a/lucene/core/src/java/org/apache/lucene/geo/Polygon.java +++ b/lucene/core/src/java/org/apache/lucene/geo/Polygon.java @@ -18,8 +18,6 @@ package org.apache.lucene.geo; import java.util.Arrays; -import org.apache.lucene.index.PointValues.Relation; - /** * Represents a closed polygon on the earth's surface. *
@@ -48,8 +46,6 @@ public final class Polygon { /** 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. */ @@ -110,200 +106,6 @@ public final class Polygon { this.maxLon = maxLon; } - /** - * Returns true if the point is contained within this polygon. - *
- * See - * https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html for more information. - */ - // ported to java from https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html - // original code under the BSD license (https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html#License%20to%20Use) - // - // Copyright (c) 1970-2003, Wm. Randolph Franklin - // - // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - // documentation files (the "Software"), to deal in the Software without restriction, including without limitation - // the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and - // to permit persons to whom the Software is furnished to do so, subject to the following conditions: - // - // 1. Redistributions of source code must retain the above copyright - // notice, this list of conditions and the following disclaimers. - // 2. Redistributions in binary form must reproduce the above copyright - // notice in the documentation and/or other materials provided with - // the distribution. - // 3. The name of W. Randolph Franklin may not be used to endorse or - // promote products derived from this Software without specific - // prior written permission. - // - // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED - // TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - // IN THE SOFTWARE. - public boolean contains(double latitude, double longitude) { - // check bounding box - if (latitude < minLat || latitude > maxLat || longitude < minLon || longitude > maxLon) { - return false; - } - - boolean inPoly = false; - boolean previous = polyLats[0] > latitude; - for (int i = 1; i < polyLats.length; i++) { - boolean current = polyLats[i] > latitude; - if (current != previous) { - if (longitude < (polyLons[i-1] - polyLons[i]) * (latitude - polyLats[i]) / (polyLats[i-1] - polyLats[i]) + polyLons[i]) { - inPoly = !inPoly; - } - previous = current; - } - } - if (inPoly) { - for (Polygon hole : holes) { - if (hole.contains(latitude, longitude)) { - return false; - } - } - return true; - } else { - return false; - } - } - - /** Returns relation to the provided rectangle */ - public Relation relate(double minLat, double maxLat, double minLon, 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 Relation.CELL_OUTSIDE_QUERY; - } - // if the rectangle fully encloses us, we cross. - if (minLat <= this.minLat && maxLat >= this.maxLat && minLon <= this.minLon && maxLon >= this.maxLon) { - return Relation.CELL_CROSSES_QUERY; - } - // check any holes - for (Polygon hole : holes) { - Relation holeRelation = hole.relate(minLat, maxLat, minLon, maxLon); - if (holeRelation == Relation.CELL_CROSSES_QUERY) { - return Relation.CELL_CROSSES_QUERY; - } else if (holeRelation == Relation.CELL_INSIDE_QUERY) { - return Relation.CELL_OUTSIDE_QUERY; - } - } - // check each corner: if < 4 are present, its cheaper than crossesSlowly - int numCorners = numberOfCorners(minLat, maxLat, minLon, maxLon); - if (numCorners == 4) { - if (crossesSlowly(minLat, maxLat, minLon, maxLon)) { - return Relation.CELL_CROSSES_QUERY; - } - return Relation.CELL_INSIDE_QUERY; - } else if (numCorners > 0) { - return Relation.CELL_CROSSES_QUERY; - } - - // we cross - if (crossesSlowly(minLat, maxLat, minLon, maxLon)) { - return Relation.CELL_CROSSES_QUERY; - } - - return Relation.CELL_OUTSIDE_QUERY; - } - - // returns 0, 4, or something in between - private int numberOfCorners(double minLat, double maxLat, double minLon, double maxLon) { - int containsCount = 0; - if (contains(minLat, minLon)) { - containsCount++; - } - if (contains(minLat, maxLon)) { - containsCount++; - } - if (containsCount == 1) { - return containsCount; - } - if (contains(maxLat, maxLon)) { - containsCount++; - } - if (containsCount == 2) { - return containsCount; - } - if (contains(maxLat, minLon)) { - containsCount++; - } - return containsCount; - } - - /** Returns true if the box crosses our polygon */ - private boolean crossesSlowly(double minLat, double maxLat, double minLon, double maxLon) { - // we compute line intersections of every polygon edge with every box line. - // if we find one, return true. - // for each box line (AB): - // for each poly line (CD): - // intersects = orient(C,D,A) * orient(C,D,B) <= 0 && orient(A,B,C) * orient(A,B,D) <= 0 - for (int i = 1; i < polyLons.length; i++) { - double cy = polyLats[i - 1]; - double dy = polyLats[i]; - double cx = polyLons[i - 1]; - double dx = polyLons[i]; - - // optimization: see if the rectangle is outside of the "bounding box" of the polyline at all - // if not, don't waste our time trying more complicated stuff - if ((cy < minLat && dy < minLat) || - (cy > maxLat && dy > maxLat) || - (cx < minLon && dx < minLon) || - (cx > maxLon && dx > maxLon)) { - continue; - } - - // does box's top edge intersect polyline? - // ax = minLon, bx = maxLon, ay = maxLat, by = maxLat - if (orient(cx, cy, dx, dy, minLon, maxLat) * orient(cx, cy, dx, dy, maxLon, maxLat) <= 0 && - orient(minLon, maxLat, maxLon, maxLat, cx, cy) * orient(minLon, maxLat, maxLon, maxLat, dx, dy) <= 0) { - return true; - } - - // does box's right edge intersect polyline? - // ax = maxLon, bx = maxLon, ay = maxLat, by = minLat - if (orient(cx, cy, dx, dy, maxLon, maxLat) * orient(cx, cy, dx, dy, maxLon, minLat) <= 0 && - orient(maxLon, maxLat, maxLon, minLat, cx, cy) * orient(maxLon, maxLat, maxLon, minLat, dx, dy) <= 0) { - return true; - } - - // does box's bottom edge intersect polyline? - // ax = maxLon, bx = minLon, ay = minLat, by = minLat - if (orient(cx, cy, dx, dy, maxLon, minLat) * orient(cx, cy, dx, dy, minLon, minLat) <= 0 && - orient(maxLon, minLat, minLon, minLat, cx, cy) * orient(maxLon, minLat, minLon, minLat, dx, dy) <= 0) { - return true; - } - - // does box's left edge intersect polyline? - // ax = minLon, bx = minLon, ay = minLat, by = maxLat - if (orient(cx, cy, dx, dy, minLon, minLat) * orient(cx, cy, dx, dy, minLon, maxLat) <= 0 && - orient(minLon, minLat, minLon, maxLat, cx, cy) * orient(minLon, minLat, minLon, maxLat, dx, dy) <= 0) { - return true; - } - } - return false; - } - - /** - * Returns a positive value if points a, b, and c are arranged in counter-clockwise order, - * negative value if clockwise, zero if collinear. - */ - // see the "Orient2D" method described here: - // http://www.cs.berkeley.edu/~jrs/meshpapers/robnotes.pdf - // https://www.cs.cmu.edu/~quake/robust.html - // Note that this one does not yet have the floating point tricks to be exact! - private static int orient(double ax, double ay, double bx, double by, double cx, double cy) { - double v1 = (bx - ax) * (cy - ay); - double v2 = (cx - ax) * (by - ay); - if (v1 > v2) { - return 1; - } else if (v1 < v2) { - return -1; - } else { - return 0; - } - } - /** Returns a copy of the internal latitude array */ public double[] getPolyLats() { return polyLats.clone(); @@ -319,29 +121,6 @@ public final class Polygon { return holes.clone(); } - /** 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; - } - - /** Returns the multipolygon relation for the rectangle */ - public static Relation relate(Polygon[] polygons, double minLat, double maxLat, double minLon, double maxLon) { - for (Polygon polygon : polygons) { - Relation relation = polygon.relate(minLat, maxLat, minLon, maxLon); - if (relation != Relation.CELL_OUTSIDE_QUERY) { - // note: we optimize for non-overlapping multipolygons. so if we cross one, - // we won't keep iterating to try to find a contains. - return relation; - } - } - return Relation.CELL_OUTSIDE_QUERY; - } - @Override public int hashCode() { final int prime = 31; diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonTree.java b/lucene/core/src/java/org/apache/lucene/geo/Polygon2D.java similarity index 72% rename from lucene/sandbox/src/java/org/apache/lucene/document/LatLonTree.java rename to lucene/core/src/java/org/apache/lucene/geo/Polygon2D.java index f7a2927e2fb..320d71a2284 100644 --- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonTree.java +++ b/lucene/core/src/java/org/apache/lucene/geo/Polygon2D.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.lucene.document; +package org.apache.lucene.geo; import java.util.Arrays; @@ -24,38 +24,55 @@ import org.apache.lucene.index.PointValues.Relation; /** * 2D polygon implementation represented as a balanced interval tree of edges. *
- * contains() and crosses() are still O(n), but for most practical polygons - * are much faster than brute force. + * Construction takes {@code O(n log n)} time for sorting and tree construction. + * {@link #contains contains()} and {@link #relate relate()} are {@code O(n)}, but for most + * practical polygons are much faster than brute force. *
* Loosely based on the algorithm described in * http://www-ma2.upc.es/geoc/Schirra-pointPolygon.pdf. + * @lucene.internal */ // Both Polygon.contains() and Polygon.crossesSlowly() loop all edges, and first check that the edge is within a range. // we just organize the edges to do the same computations on the same subset of edges more efficiently. -// TODO: clean this up, call it Polygon2D, and remove all the 2D methods from Polygon? -final class LatLonTree { - private final LatLonTree[] holes; - +public final class Polygon2D { /** minimum latitude of this polygon's bounding box area */ - final double minLat; + public final double minLat; /** maximum latitude of this polygon's bounding box area */ - final double maxLat; + public final double maxLat; /** minimum longitude of this polygon's bounding box area */ - final double minLon; + public final double minLon; /** maximum longitude of this polygon's bounding box area */ - final double maxLon; + public final double maxLon; - /** root node of our tree */ - final Edge tree; + // each component/hole is a node in an augmented 2d kd-tree: we alternate splitting between latitude/longitude, + // and pull up max values for both dimensions to each parent node (regardless of split). - // TODO: "pack" all the gons and holes into one tree with separator. - // the algorithms support this, but we have to be careful. - LatLonTree(Polygon polygon, LatLonTree... holes) { - this.holes = holes.clone(); + /** maximum latitude of this component or any of its children */ + private double maxY; + /** maximum longitude of this component or any of its children */ + private double maxX; + /** which dimension was this node split on */ + // TODO: its implicit based on level, but boolean keeps code simple + private boolean splitX; + + // child components, or null + private Polygon2D left; + private Polygon2D right; + + /** tree of holes, or null */ + private final Polygon2D holes; + + /** root node of edge tree */ + private final Edge tree; + + private Polygon2D(Polygon polygon, Polygon2D holes) { + this.holes = holes; this.minLat = polygon.minLat; this.maxLat = polygon.maxLat; this.minLon = polygon.minLon; this.maxLon = polygon.maxLon; + this.maxY = maxLat; + this.maxX = maxLon; // create interval tree of edges this.tree = createTree(polygon.getPolyLats(), polygon.getPolyLons()); @@ -67,17 +84,35 @@ final class LatLonTree { * See * https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html for more information. */ - boolean contains(double latitude, double longitude) { + public boolean contains(double latitude, double longitude) { + if (latitude <= maxY && longitude <= maxX) { + if (componentContains(latitude, longitude)) { + return true; + } + if (left != null) { + if (left.contains(latitude, longitude)) { + return true; + } + } + if (right != null && ((splitX == false && latitude >= minLat) || (splitX && longitude >= minLon))) { + if (right.contains(latitude, longitude)) { + return true; + } + } + } + return false; + } + + /** Returns true if the point is contained within this polygon component. */ + private boolean componentContains(double latitude, double longitude) { // check bounding box if (latitude < minLat || latitude > maxLat || longitude < minLon || longitude > maxLon) { return false; } if (tree.contains(latitude, longitude)) { - for (LatLonTree hole : holes) { - if (hole.contains(latitude, longitude)) { - return false; - } + if (holes != null && holes.contains(latitude, longitude)) { + return false; } return true; } @@ -86,7 +121,30 @@ final class LatLonTree { } /** Returns relation to the provided rectangle */ - Relation relate(double minLat, double maxLat, double minLon, double maxLon) { + public Relation relate(double minLat, double maxLat, double minLon, double maxLon) { + if (minLat <= maxY && minLon <= maxX) { + Relation relation = componentRelate(minLat, maxLat, minLon, maxLon); + if (relation != Relation.CELL_OUTSIDE_QUERY) { + return relation; + } + if (left != null) { + relation = left.relate(minLat, maxLat, minLon, maxLon); + if (relation != Relation.CELL_OUTSIDE_QUERY) { + return relation; + } + } + if (right != null && ((splitX == false && maxLat >= this.minLat) || (splitX && maxLon >= this.minLon))) { + relation = right.relate(minLat, maxLat, minLon, maxLon); + if (relation != Relation.CELL_OUTSIDE_QUERY) { + return relation; + } + } + } + return Relation.CELL_OUTSIDE_QUERY; + } + + /** Returns relation to the provided rectangle for this component */ + private Relation componentRelate(double minLat, double maxLat, double minLon, 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 Relation.CELL_OUTSIDE_QUERY; @@ -96,8 +154,8 @@ final class LatLonTree { return Relation.CELL_CROSSES_QUERY; } // check any holes - for (LatLonTree hole : holes) { - Relation holeRelation = hole.relate(minLat, maxLat, minLon, maxLon); + if (holes != null) { + Relation holeRelation = holes.relate(minLat, maxLat, minLon, maxLon); if (holeRelation == Relation.CELL_CROSSES_QUERY) { return Relation.CELL_CROSSES_QUERY; } else if (holeRelation == Relation.CELL_INSIDE_QUERY) { @@ -126,66 +184,85 @@ final class LatLonTree { // returns 0, 4, or something in between private int numberOfCorners(double minLat, double maxLat, double minLon, double maxLon) { int containsCount = 0; - if (contains(minLat, minLon)) { + if (componentContains(minLat, minLon)) { containsCount++; } - if (contains(minLat, maxLon)) { + if (componentContains(minLat, maxLon)) { containsCount++; } if (containsCount == 1) { return containsCount; } - if (contains(maxLat, maxLon)) { + if (componentContains(maxLat, maxLon)) { containsCount++; } if (containsCount == 2) { return containsCount; } - if (contains(maxLat, minLon)) { + if (componentContains(maxLat, minLon)) { containsCount++; } return containsCount; } - - /** Helper for multipolygon logic: returns true if any of the supplied polygons contain the point */ - static boolean contains(LatLonTree[] polygons, double latitude, double longitude) { - for (LatLonTree polygon : polygons) { - if (polygon.contains(latitude, longitude)) { - return true; + + /** Creates tree from sorted components (with range low and high inclusive) */ + private static Polygon2D createTree(Polygon2D components[], int low, int high, boolean splitX) { + if (low > high) { + return null; + } else if (low < high) { + // TODO: do one sort instead! there are better algorithms! + if (splitX) { + Arrays.sort(components, low, high+1, (left, right) -> { + int ret = Double.compare(left.minLon, right.minLon); + if (ret == 0) { + ret = Double.compare(left.maxX, right.maxX); + } + return ret; + }); + } else { + Arrays.sort(components, low, high+1, (left, right) -> { + int ret = Double.compare(left.minLat, right.minLat); + if (ret == 0) { + ret = Double.compare(left.maxY, right.maxY); + } + return ret; + }); } } - return false; - } - - /** Returns the multipolygon relation for the rectangle */ - static Relation relate(LatLonTree[] polygons, double minLat, double maxLat, double minLon, double maxLon) { - for (LatLonTree polygon : polygons) { - Relation relation = polygon.relate(minLat, maxLat, minLon, maxLon); - if (relation != Relation.CELL_OUTSIDE_QUERY) { - // note: we optimize for non-overlapping multipolygons. so if we cross one, - // we won't keep iterating to try to find a contains. - return relation; - } + // add midpoint + int mid = (low + high) >>> 1; + Polygon2D newNode = components[mid]; + newNode.splitX = splitX; + // add children + newNode.left = createTree(components, low, mid - 1, !splitX); + newNode.right = createTree(components, mid + 1, high, !splitX); + // pull up max values to this node + if (newNode.left != null) { + newNode.maxX = Math.max(newNode.maxX, newNode.left.maxX); + newNode.maxY = Math.max(newNode.maxY, newNode.left.maxY); } - return Relation.CELL_OUTSIDE_QUERY; + if (newNode.right != null) { + newNode.maxX = Math.max(newNode.maxX, newNode.right.maxX); + newNode.maxY = Math.max(newNode.maxY, newNode.right.maxY); + } + return newNode; } - /** Builds a tree from multipolygon */ - static LatLonTree[] build(Polygon... polygons) { - // TODO: use one tree with separators (carefully!) - LatLonTree trees[] = new LatLonTree[polygons.length]; - for (int i = 0; i < trees.length; i++) { + /** Builds a Polygon2D from multipolygon */ + public static Polygon2D create(Polygon... polygons) { + Polygon2D components[] = new Polygon2D[polygons.length]; + for (int i = 0; i < components.length; i++) { Polygon gon = polygons[i]; Polygon gonHoles[] = gon.getHoles(); - LatLonTree holes[] = new LatLonTree[gonHoles.length]; - for (int j = 0; j < holes.length; j++) { - holes[j] = new LatLonTree(gonHoles[j]); + Polygon2D holes = null; + if (gonHoles.length > 0) { + holes = create(gonHoles); } - trees[i] = new LatLonTree(gon, holes); + components[i] = new Polygon2D(gon, holes); } - return trees; + return createTree(components, 0, components.length - 1, false); } - + /** * Internal tree node: represents polygon edge from lat1,lon1 to lat2,lon2. * The sort value is {@code low}, which is the minimum latitude of the edge. diff --git a/lucene/core/src/test/org/apache/lucene/geo/TestPolygon.java b/lucene/core/src/test/org/apache/lucene/geo/TestPolygon.java index 12c36900443..401092f5d50 100644 --- a/lucene/core/src/test/org/apache/lucene/geo/TestPolygon.java +++ b/lucene/core/src/test/org/apache/lucene/geo/TestPolygon.java @@ -16,17 +16,8 @@ */ package org.apache.lucene.geo; -import org.apache.lucene.geo.Polygon; -import org.apache.lucene.index.PointValues.Relation; import org.apache.lucene.util.LuceneTestCase; -import static org.apache.lucene.geo.GeoTestUtil.nextLatitude; -import static org.apache.lucene.geo.GeoTestUtil.nextLongitude; -import static org.apache.lucene.geo.GeoTestUtil.nextPolygon; - -import java.util.ArrayList; -import java.util.List; - public class TestPolygon extends LuceneTestCase { /** null polyLats not allowed */ @@ -68,325 +59,4 @@ public class TestPolygon extends LuceneTestCase { }); 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 - - // relate(box): this can conservatively return CELL_CROSSES_QUERY - assertEquals(Relation.CELL_INSIDE_QUERY, Polygon.relate(polygons, -2, 2, -2, 2)); // on the island - assertEquals(Relation.CELL_OUTSIDE_QUERY, Polygon.relate(polygons, 6, 7, 6, 7)); // in the hole - assertEquals(Relation.CELL_INSIDE_QUERY, Polygon.relate(polygons, 24, 25, 24, 25)); // on the mainland - assertEquals(Relation.CELL_OUTSIDE_QUERY, Polygon.relate(polygons, 51, 52, 51, 52)); // in the ocean - assertEquals(Relation.CELL_CROSSES_QUERY, Polygon.relate(polygons, -60, 60, -60, 60)); // enclosing us completely - assertEquals(Relation.CELL_CROSSES_QUERY, Polygon.relate(polygons, 49, 51, 49, 51)); // overlapping the mainland - assertEquals(Relation.CELL_CROSSES_QUERY, Polygon.relate(polygons, 9, 11, 9, 11)); // overlapping the hole - assertEquals(Relation.CELL_CROSSES_QUERY, Polygon.relate(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); - assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(yMin, yMax, xMin, xMax)); - } - - public void testBoundingBox() throws Exception { - for (int i = 0; i < 100; i++) { - Polygon polygon = nextPolygon(); - - for (int j = 0; j < 100; j++) { - double latitude = nextLatitude(); - double longitude = 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); - } - } - } - } - - // targets the bounding box directly - public void testBoundingBoxEdgeCases() throws Exception { - for (int i = 0; i < 100; i++) { - Polygon polygon = nextPolygon(); - - for (int j = 0; j < 100; j++) { - double point[] = GeoTestUtil.nextPointNear(polygon); - double latitude = point[0]; - double longitude = point[1]; - // 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); - } - } - } - } - - /** If polygon.contains(box) returns true, then any point in that box should return true as well */ - public void testContainsRandom() throws Exception { - for (int i = 0; i < 1000; i++) { - Polygon polygon = nextPolygon(); - - for (int j = 0; j < 100; j++) { - Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); - // allowed to conservatively return false - if (polygon.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == Relation.CELL_INSIDE_QUERY) { - for (int k = 0; k < 500; k++) { - // this tests in our range but sometimes outside! so we have to double-check its really in other box - double point[] = GeoTestUtil.nextPointNear(rectangle); - double latitude = point[0]; - double longitude = point[1]; - // check for sure its in our box - if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { - assertTrue(polygon.contains(latitude, longitude)); - } - } - for (int k = 0; k < 100; k++) { - // this tests in our range but sometimes outside! so we have to double-check its really in other box - double point[] = GeoTestUtil.nextPointNear(polygon); - double latitude = point[0]; - double longitude = point[1]; - // check for sure its in our box - if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { - assertTrue(polygon.contains(latitude, longitude)); - } - } - } - } - } - } - - /** If polygon.contains(box) returns true, then any point in that box should return true as well */ - // different from testContainsRandom in that its not a purely random test. we iterate the vertices of the polygon - // and generate boxes near each one of those to try to be more efficient. - public void testContainsEdgeCases() throws Exception { - for (int i = 0; i < 1000; i++) { - Polygon polygon = nextPolygon(); - - for (int j = 0; j < 10; j++) { - Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); - // allowed to conservatively return false - if (polygon.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == Relation.CELL_INSIDE_QUERY) { - for (int k = 0; k < 100; k++) { - // this tests in our range but sometimes outside! so we have to double-check its really in other box - double point[] = GeoTestUtil.nextPointNear(rectangle); - double latitude = point[0]; - double longitude = point[1]; - // check for sure its in our box - if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { - assertTrue(polygon.contains(latitude, longitude)); - } - } - for (int k = 0; k < 20; k++) { - // this tests in our range but sometimes outside! so we have to double-check its really in other box - double point[] = GeoTestUtil.nextPointNear(polygon); - double latitude = point[0]; - double longitude = point[1]; - // check for sure its in our box - if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { - assertTrue(polygon.contains(latitude, longitude)); - } - } - } - } - } - } - - /** If polygon.intersects(box) returns false, then any point in that box should return false as well */ - public void testIntersectRandom() { - for (int i = 0; i < 100; i++) { - Polygon polygon = nextPolygon(); - - for (int j = 0; j < 100; j++) { - Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); - // allowed to conservatively return true. - if (polygon.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == Relation.CELL_OUTSIDE_QUERY) { - for (int k = 0; k < 1000; k++) { - double point[] = GeoTestUtil.nextPointNear(rectangle); - // this tests in our range but sometimes outside! so we have to double-check its really in other box - double latitude = point[0]; - double longitude = point[1]; - // check for sure its in our box - if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { - assertFalse(polygon.contains(latitude, longitude)); - } - } - for (int k = 0; k < 100; k++) { - double point[] = GeoTestUtil.nextPointNear(polygon); - // this tests in our range but sometimes outside! so we have to double-check its really in other box - double latitude = point[0]; - double longitude = point[1]; - // check for sure its in our box - if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { - assertFalse(polygon.contains(latitude, longitude)); - } - } - } - } - } - } - - /** If polygon.intersects(box) returns false, then any point in that box should return false as well */ - // different from testIntersectsRandom in that its not a purely random test. we iterate the vertices of the polygon - // and generate boxes near each one of those to try to be more efficient. - public void testIntersectEdgeCases() { - for (int i = 0; i < 100; i++) { - Polygon polygon = nextPolygon(); - - for (int j = 0; j < 10; j++) { - Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); - // allowed to conservatively return false. - if (polygon.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == Relation.CELL_OUTSIDE_QUERY) { - for (int k = 0; k < 100; k++) { - // this tests in our range but sometimes outside! so we have to double-check its really in other box - double point[] = GeoTestUtil.nextPointNear(rectangle); - double latitude = point[0]; - double longitude = point[1]; - // check for sure its in our box - if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { - assertFalse(polygon.contains(latitude, longitude)); - } - } - for (int k = 0; k < 50; k++) { - // this tests in our range but sometimes outside! so we have to double-check its really in other box - double point[] = GeoTestUtil.nextPointNear(polygon); - double latitude = point[0]; - double longitude = point[1]; - // check for sure its in our box - if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { - assertFalse(polygon.contains(latitude, longitude)); - } - } - } - } - } - } - - /** Tests edge case behavior with respect to insideness */ - public void testEdgeInsideness() { - Polygon poly = new Polygon(new double[] { -2, -2, 2, 2, -2 }, new double[] { -2, 2, 2, -2, -2 }); - assertTrue(poly.contains(-2, -2)); // bottom left corner: true - assertFalse(poly.contains(-2, 2)); // bottom right corner: false - assertFalse(poly.contains(2, -2)); // top left corner: false - assertFalse(poly.contains(2, 2)); // top right corner: false - assertTrue(poly.contains(-2, -1)); // bottom side: true - assertTrue(poly.contains(-2, 0)); // bottom side: true - assertTrue(poly.contains(-2, 1)); // bottom side: true - assertFalse(poly.contains(2, -1)); // top side: false - assertFalse(poly.contains(2, 0)); // top side: false - assertFalse(poly.contains(2, 1)); // top side: false - assertFalse(poly.contains(-1, 2)); // right side: false - assertFalse(poly.contains(0, 2)); // right side: false - assertFalse(poly.contains(1, 2)); // right side: false - assertTrue(poly.contains(-1, -2)); // left side: true - assertTrue(poly.contains(0, -2)); // left side: true - assertTrue(poly.contains(1, -2)); // left side: true - } - - /** Tests that our impl supports multiple components and holes (not currently used) */ - public void testMultiPolygonContains() { - // this is the equivalent of the following: we don't recommend anyone do this (e.g. relation logic will not work) - // but lets not lose the property that it works. - /// - // 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 }; - - Polygon polygon = new Polygon(new double[] { 0, -50, -50, 50, 50, -50, 0, -5, -5, 5, 5, -5, 0, -10, -10, 10, 10, -10, 0 }, - new double[] { 0, -50, 50, 50, -50, -50, 0, -5, 5, 5, -5, -5, 0, -10, 10, 10, -10, -10, 0 }); - - assertTrue(polygon.contains(-2, 2)); // on the island - assertFalse(polygon.contains(-6, 6)); // in the hole - assertTrue(polygon.contains(-25, 25)); // on the mainland - assertFalse(polygon.contains(-51, 51)); // in the ocean - } - - /** Tests current impl against original algorithm */ - public void testContainsAgainstOriginal() { - for (int i = 0; i < 1000; i++) { - Polygon polygon = nextPolygon(); - // currently we don't generate these, but this test does not want holes. - while (polygon.getHoles().length > 0) { - polygon = nextPolygon(); - } - - double polyLats[] = polygon.getPolyLats(); - double polyLons[] = polygon.getPolyLons(); - - // random lat/lons against polygon - for (int j = 0; j < 1000; j++) { - double point[] = GeoTestUtil.nextPointNear(polygon); - double latitude = point[0]; - double longitude = point[1]; - // bounding box check required due to rounding errors (we don't solve that problem) - if (latitude >= polygon.minLat && latitude <= polygon.maxLat && longitude >= polygon.minLon && longitude <= polygon.maxLon) { - boolean expected = containsOriginal(polyLats, polyLons, latitude, longitude); - assertEquals(expected, polygon.contains(latitude, longitude)); - } - } - } - } - - // direct port of PNPOLY C code (https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html) - // this allows us to improve the code yet still ensure we have its properties - // it is under the BSD license (https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html#License%20to%20Use) - // - // Copyright (c) 1970-2003, Wm. Randolph Franklin - // - // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - // documentation files (the "Software"), to deal in the Software without restriction, including without limitation - // the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and - // to permit persons to whom the Software is furnished to do so, subject to the following conditions: - // - // 1. Redistributions of source code must retain the above copyright - // notice, this list of conditions and the following disclaimers. - // 2. Redistributions in binary form must reproduce the above copyright - // notice in the documentation and/or other materials provided with - // the distribution. - // 3. The name of W. Randolph Franklin may not be used to endorse or - // promote products derived from this Software without specific - // prior written permission. - // - // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED - // TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF - // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - // IN THE SOFTWARE. - private static boolean containsOriginal(double polyLats[], double polyLons[], double latitude, double longitude) { - boolean c = false; - int i, j; - int nvert = polyLats.length; - double verty[] = polyLats; - double vertx[] = polyLons; - double testy = latitude; - double testx = longitude; - for (i = 0, j = nvert-1; i < nvert; j = i++) { - if ( ((verty[i]>testy) != (verty[j]>testy)) && - (testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i]) ) - c = !c; - } - return c; - } } diff --git a/lucene/core/src/test/org/apache/lucene/geo/TestPolygon2D.java b/lucene/core/src/test/org/apache/lucene/geo/TestPolygon2D.java new file mode 100644 index 00000000000..70281cac1d4 --- /dev/null +++ b/lucene/core/src/test/org/apache/lucene/geo/TestPolygon2D.java @@ -0,0 +1,289 @@ +/* + * 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.geo; + +import static org.apache.lucene.geo.GeoTestUtil.nextLatitude; +import static org.apache.lucene.geo.GeoTestUtil.nextLongitude; +import static org.apache.lucene.geo.GeoTestUtil.nextPolygon; + +import org.apache.lucene.index.PointValues.Relation; +import org.apache.lucene.util.LuceneTestCase; + +/** Test Polygon2D impl */ +public class TestPolygon2D extends LuceneTestCase { + + /** 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 } ); + Polygon2D polygon = Polygon2D.create(outer, island); + + // contains(point) + assertTrue(polygon.contains(-2, 2)); // on the island + assertFalse(polygon.contains(-6, 6)); // in the hole + assertTrue(polygon.contains(-25, 25)); // on the mainland + assertFalse(polygon.contains(-51, 51)); // in the ocean + + // relate(box): this can conservatively return CELL_CROSSES_QUERY + assertEquals(Relation.CELL_INSIDE_QUERY, polygon.relate(-2, 2, -2, 2)); // on the island + assertEquals(Relation.CELL_OUTSIDE_QUERY, polygon.relate(6, 7, 6, 7)); // in the hole + assertEquals(Relation.CELL_INSIDE_QUERY, polygon.relate(24, 25, 24, 25)); // on the mainland + assertEquals(Relation.CELL_OUTSIDE_QUERY, polygon.relate(51, 52, 51, 52)); // in the ocean + assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(-60, 60, -60, 60)); // enclosing us completely + assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(49, 51, 49, 51)); // overlapping the mainland + assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(9, 11, 9, 11)); // overlapping the hole + assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(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 + Polygon2D polygon = Polygon2D.create(new Polygon(py, px)); + assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(yMin, yMax, xMin, xMax)); + } + + public void testBoundingBox() throws Exception { + for (int i = 0; i < 100; i++) { + Polygon2D polygon = Polygon2D.create(nextPolygon()); + + for (int j = 0; j < 100; j++) { + double latitude = nextLatitude(); + double longitude = 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); + } + } + } + } + + // targets the bounding box directly + public void testBoundingBoxEdgeCases() throws Exception { + for (int i = 0; i < 100; i++) { + Polygon polygon = nextPolygon(); + Polygon2D impl = Polygon2D.create(polygon); + + for (int j = 0; j < 100; j++) { + double point[] = GeoTestUtil.nextPointNear(polygon); + double latitude = point[0]; + double longitude = point[1]; + // if the point is within poly, then it should be in our bounding box + if (impl.contains(latitude, longitude)) { + assertTrue(latitude >= polygon.minLat && latitude <= polygon.maxLat); + assertTrue(longitude >= polygon.minLon && longitude <= polygon.maxLon); + } + } + } + } + + /** If polygon.contains(box) returns true, then any point in that box should return true as well */ + public void testContainsRandom() throws Exception { + for (int i = 0; i < 1000; i++) { + Polygon polygon = nextPolygon(); + Polygon2D impl = Polygon2D.create(polygon); + + for (int j = 0; j < 100; j++) { + Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); + // allowed to conservatively return false + if (impl.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == Relation.CELL_INSIDE_QUERY) { + for (int k = 0; k < 500; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(rectangle); + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertTrue(impl.contains(latitude, longitude)); + } + } + for (int k = 0; k < 100; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(polygon); + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertTrue(impl.contains(latitude, longitude)); + } + } + } + } + } + } + + /** If polygon.contains(box) returns true, then any point in that box should return true as well */ + // different from testContainsRandom in that its not a purely random test. we iterate the vertices of the polygon + // and generate boxes near each one of those to try to be more efficient. + public void testContainsEdgeCases() throws Exception { + for (int i = 0; i < 1000; i++) { + Polygon polygon = nextPolygon(); + Polygon2D impl = Polygon2D.create(polygon); + + for (int j = 0; j < 10; j++) { + Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); + // allowed to conservatively return false + if (impl.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == Relation.CELL_INSIDE_QUERY) { + for (int k = 0; k < 100; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(rectangle); + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertTrue(impl.contains(latitude, longitude)); + } + } + for (int k = 0; k < 20; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(polygon); + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertTrue(impl.contains(latitude, longitude)); + } + } + } + } + } + } + + /** If polygon.intersects(box) returns false, then any point in that box should return false as well */ + public void testIntersectRandom() { + for (int i = 0; i < 100; i++) { + Polygon polygon = nextPolygon(); + Polygon2D impl = Polygon2D.create(polygon); + + for (int j = 0; j < 100; j++) { + Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); + // allowed to conservatively return true. + if (impl.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == Relation.CELL_OUTSIDE_QUERY) { + for (int k = 0; k < 1000; k++) { + double point[] = GeoTestUtil.nextPointNear(rectangle); + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertFalse(impl.contains(latitude, longitude)); + } + } + for (int k = 0; k < 100; k++) { + double point[] = GeoTestUtil.nextPointNear(polygon); + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertFalse(impl.contains(latitude, longitude)); + } + } + } + } + } + } + + /** If polygon.intersects(box) returns false, then any point in that box should return false as well */ + // different from testIntersectsRandom in that its not a purely random test. we iterate the vertices of the polygon + // and generate boxes near each one of those to try to be more efficient. + public void testIntersectEdgeCases() { + for (int i = 0; i < 100; i++) { + Polygon polygon = nextPolygon(); + Polygon2D impl = Polygon2D.create(polygon); + + for (int j = 0; j < 10; j++) { + Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon); + // allowed to conservatively return false. + if (impl.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon) == Relation.CELL_OUTSIDE_QUERY) { + for (int k = 0; k < 100; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(rectangle); + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertFalse(impl.contains(latitude, longitude)); + } + } + for (int k = 0; k < 50; k++) { + // this tests in our range but sometimes outside! so we have to double-check its really in other box + double point[] = GeoTestUtil.nextPointNear(polygon); + double latitude = point[0]; + double longitude = point[1]; + // check for sure its in our box + if (latitude >= rectangle.minLat && latitude <= rectangle.maxLat && longitude >= rectangle.minLon && longitude <= rectangle.maxLon) { + assertFalse(impl.contains(latitude, longitude)); + } + } + } + } + } + } + + /** Tests edge case behavior with respect to insideness */ + public void testEdgeInsideness() { + Polygon2D poly = Polygon2D.create(new Polygon(new double[] { -2, -2, 2, 2, -2 }, new double[] { -2, 2, 2, -2, -2 })); + assertTrue(poly.contains(-2, -2)); // bottom left corner: true + assertFalse(poly.contains(-2, 2)); // bottom right corner: false + assertFalse(poly.contains(2, -2)); // top left corner: false + assertFalse(poly.contains(2, 2)); // top right corner: false + assertTrue(poly.contains(-2, -1)); // bottom side: true + assertTrue(poly.contains(-2, 0)); // bottom side: true + assertTrue(poly.contains(-2, 1)); // bottom side: true + assertFalse(poly.contains(2, -1)); // top side: false + assertFalse(poly.contains(2, 0)); // top side: false + assertFalse(poly.contains(2, 1)); // top side: false + assertFalse(poly.contains(-1, 2)); // right side: false + assertFalse(poly.contains(0, 2)); // right side: false + assertFalse(poly.contains(1, 2)); // right side: false + assertTrue(poly.contains(-1, -2)); // left side: true + assertTrue(poly.contains(0, -2)); // left side: true + assertTrue(poly.contains(1, -2)); // left side: true + } + + /** Tests current impl against original algorithm */ + public void testContainsAgainstOriginal() { + for (int i = 0; i < 1000; i++) { + Polygon polygon = nextPolygon(); + // currently we don't generate these, but this test does not want holes. + while (polygon.getHoles().length > 0) { + polygon = nextPolygon(); + } + Polygon2D impl = Polygon2D.create(polygon); + + // random lat/lons against polygon + for (int j = 0; j < 1000; j++) { + double point[] = GeoTestUtil.nextPointNear(polygon); + double latitude = point[0]; + double longitude = point[1]; + boolean expected = GeoTestUtil.containsSlowly(polygon, latitude, longitude); + assertEquals(expected, impl.contains(latitude, longitude)); + } + } + } +} diff --git a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonGrid.java b/lucene/sandbox/src/java/org/apache/lucene/document/LatLonGrid.java deleted file mode 100644 index 4b3b2b2acb9..00000000000 --- a/lucene/sandbox/src/java/org/apache/lucene/document/LatLonGrid.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * 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.document; - -import org.apache.lucene.geo.Polygon; -import org.apache.lucene.index.PointValues.Relation; -import org.apache.lucene.util.FixedBitSet; - -import static org.apache.lucene.geo.GeoEncodingUtils.decodeLatitude; -import static org.apache.lucene.geo.GeoEncodingUtils.decodeLongitude; - -/** - * This is a temporary hack, until some polygon methods have better performance! - *
- * When this file is removed then we have made good progress! In general we don't call - * the point-in-polygon algorithm that much, because of how BKD divides up the data. But - * today the method is very slow (general to all polygons, linear with the number of vertices). - * At the same time polygon-rectangle relation operations are also slow in the same way, this - * just really ensures they are the bottleneck by removing most of the point-in-polygon calls. - *
- * See the "grid" algorithm description here: http://erich.realtimerendering.com/ptinpoly/ - * A few differences: - *