From c98229948a0bfdd9fd4b845c4110be0a5246e09e Mon Sep 17 00:00:00 2001 From: Ignacio Vera Date: Wed, 29 Jan 2020 19:24:51 +0100 Subject: [PATCH] LUCENE-9152: Improve line intersection detection for polygons (#1187) --- lucene/CHANGES.txt | 2 + .../java/org/apache/lucene/geo/EdgeTree.java | 21 ++++++---- .../java/org/apache/lucene/geo/Line2D.java | 12 +++--- .../java/org/apache/lucene/geo/Polygon2D.java | 12 +++--- .../lucene/document/TestLatLonShape.java | 42 +++++++++++++++++++ .../org/apache/lucene/geo/TestPolygon2D.java | 17 ++++++++ 6 files changed, 87 insertions(+), 19 deletions(-) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index 53a7f7a7f2a..38e23b37a28 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -132,6 +132,8 @@ Improvements * LUCENE-4702: Better compression of terms dictionaries. (Adrien Grand) +* LUCENE-9152: Improve line intersections with polygons when they are touching from the outside. (Ignacio Vera) + Optimizations --------------------- diff --git a/lucene/core/src/java/org/apache/lucene/geo/EdgeTree.java b/lucene/core/src/java/org/apache/lucene/geo/EdgeTree.java index 239dfcf3d18..f13e1a819d4 100644 --- a/lucene/core/src/java/org/apache/lucene/geo/EdgeTree.java +++ b/lucene/core/src/java/org/apache/lucene/geo/EdgeTree.java @@ -261,7 +261,7 @@ public class EdgeTree { } /** Returns true if the line crosses any edge in this edge subtree */ - protected boolean crossesLine(double minX, double maxX, double minY, double maxY, double a2x, double a2y, double b2x, double b2y) { + protected boolean crossesLine(double minX, double maxX, double minY, double maxY, double a2x, double a2y, double b2x, double b2y, boolean includeBoundary) { if (minY <= max) { double a1x = x1; double a1y = y1; @@ -272,14 +272,21 @@ public class EdgeTree { (a1y > maxY && b1y > maxY) || (a1x < minX && b1x < minX) || (a1x > maxX && b1x > maxX); - if (outside == false && lineCrossesLineWithBoundary(a1x, a1y, b1x, b1y, a2x, a2y, b2x, b2y)) { + if (outside == false) { + if (includeBoundary) { + if (lineCrossesLineWithBoundary(a1x, a1y, b1x, b1y, a2x, a2y, b2x, b2y)) { + return true; + } + } else { + if (lineCrossesLine(a1x, a1y, b1x, b1y, a2x, a2y, b2x, b2y)) { + return true; + } + } + } + if (left != null && left.crossesLine(minX, maxX, minY, maxY, a2x, a2y, b2x, b2y, includeBoundary)) { return true; } - - if (left != null && left.crossesLine(minX, maxX, minY, maxY, a2x, a2y, b2x, b2y)) { - return true; - } - if (right != null && maxY >= low && right.crossesLine(minX, maxX, minY, maxY, a2x, a2y, b2x, b2y)) { + if (right != null && maxY >= low && right.crossesLine(minX, maxX, minY, maxY, a2x, a2y, b2x, b2y, includeBoundary)) { return true; } } diff --git a/lucene/core/src/java/org/apache/lucene/geo/Line2D.java b/lucene/core/src/java/org/apache/lucene/geo/Line2D.java index 04deee97476..ed247bf7b3b 100644 --- a/lucene/core/src/java/org/apache/lucene/geo/Line2D.java +++ b/lucene/core/src/java/org/apache/lucene/geo/Line2D.java @@ -109,19 +109,19 @@ public final class Line2D implements Component2D { } } else if (ax == cx && ay == cy) { // indexed "triangle" is a line: - if (tree.crossesLine(minX, maxX, minY, maxY, ax, ay, bx, by)) { + if (tree.crossesLine(minX, maxX, minY, maxY, ax, ay, bx, by, true)) { return Relation.CELL_CROSSES_QUERY; } return Relation.CELL_OUTSIDE_QUERY; } else if (ax == bx && ay == by) { // indexed "triangle" is a line: - if (tree.crossesLine(minX, maxX, minY, maxY, bx, by, cx, cy)) { + if (tree.crossesLine(minX, maxX, minY, maxY, bx, by, cx, cy, true)) { return Relation.CELL_CROSSES_QUERY; } return Relation.CELL_OUTSIDE_QUERY; } else if (bx == cx && by == cy) { // indexed "triangle" is a line: - if (tree.crossesLine(minX, maxX, minY, maxY, cx, cy, ax, ay)) { + if (tree.crossesLine(minX, maxX, minY, maxY, cx, cy, ax, ay, true)) { return Relation.CELL_CROSSES_QUERY; } return Relation.CELL_OUTSIDE_QUERY; @@ -149,7 +149,7 @@ public final class Line2D implements Component2D { // if any of the edges intersects an the edge belongs to the shape then it cannot be within. // if it only intersects edges that do not belong to the shape, then it is a candidate // we skip edges at the dateline to support shapes crossing it - if (tree.crossesLine(minX, maxX, minY, maxY, ax, ay, bx, by)) { + if (tree.crossesLine(minX, maxX, minY, maxY, ax, ay, bx, by, true)) { if (ab == true) { return WithinRelation.NOTWITHIN; } else { @@ -157,14 +157,14 @@ public final class Line2D implements Component2D { } } - if (tree.crossesLine(minX, maxX, minY, maxY, bx, by, cx, cy)) { + if (tree.crossesLine(minX, maxX, minY, maxY, bx, by, cx, cy, true)) { if (bc == true) { return WithinRelation.NOTWITHIN; } else { relation = WithinRelation.CANDIDATE; } } - if (tree.crossesLine(minX, maxX, minY, maxY, cx, cy, ax, ay)) { + if (tree.crossesLine(minX, maxX, minY, maxY, cx, cy, ax, ay, true)) { if (ca == true) { return WithinRelation.NOTWITHIN; } else { diff --git a/lucene/core/src/java/org/apache/lucene/geo/Polygon2D.java b/lucene/core/src/java/org/apache/lucene/geo/Polygon2D.java index e6eefd84d59..c36d6aad659 100644 --- a/lucene/core/src/java/org/apache/lucene/geo/Polygon2D.java +++ b/lucene/core/src/java/org/apache/lucene/geo/Polygon2D.java @@ -187,7 +187,7 @@ public class Polygon2D implements Component2D { // if any of the edges intersects an the edge belongs to the shape then it cannot be within. // if it only intersects edges that do not belong to the shape, then it is a candidate // we skip edges at the dateline to support shapes crossing it - if (tree.crossesLine(minX, maxX, minY, maxY, ax, ay, bx, by)) { + if (tree.crossesLine(minX, maxX, minY, maxY, ax, ay, bx, by, true)) { if (ab == true) { return WithinRelation.NOTWITHIN; } else { @@ -195,14 +195,14 @@ public class Polygon2D implements Component2D { } } - if (tree.crossesLine(minX, maxX, minY, maxY, bx, by, cx, cy)) { + if (tree.crossesLine(minX, maxX, minY, maxY, bx, by, cx, cy, true)) { if (bc == true) { return WithinRelation.NOTWITHIN; } else { relation = WithinRelation.CANDIDATE; } } - if (tree.crossesLine(minX, maxX, minY, maxY, cx, cy, ax, ay)) { + if (tree.crossesLine(minX, maxX, minY, maxY, cx, cy, ax, ay, true)) { if (ca == true) { return WithinRelation.NOTWITHIN; } else { @@ -236,12 +236,12 @@ public class Polygon2D implements Component2D { } if (numCorners == 2) { - if (tree.crossesLine(minX, maxX, minY, maxY, a2x, a2y, b2x, b2y)) { + if (tree.crossesLine(minX, maxX, minY, maxY, a2x, a2y, b2x, b2y, false)) { return Relation.CELL_CROSSES_QUERY; } return Relation.CELL_INSIDE_QUERY; } else if (numCorners == 0) { - if (tree.crossesLine(minX, maxX, minY, maxY, a2x, a2y, b2x, b2y)) { + if (tree.crossesLine(minX, maxX, minY, maxY, a2x, a2y, b2x, b2y, true)) { return Relation.CELL_CROSSES_QUERY; } return Relation.CELL_OUTSIDE_QUERY; @@ -263,7 +263,7 @@ public class Polygon2D implements Component2D { if (Component2D.pointInTriangle(minX, maxX, minY, maxY, tree.x1, tree.y1, ax, ay, bx, by, cx, cy) == true) { return Relation.CELL_CROSSES_QUERY; } - if (tree.crossesTriangle(minX, maxX, minY, maxY, ax, ay, bx, by, cx, cy, false)) { + if (tree.crossesTriangle(minX, maxX, minY, maxY, ax, ay, bx, by, cx, cy, true)) { return Relation.CELL_CROSSES_QUERY; } return Relation.CELL_OUTSIDE_QUERY; diff --git a/lucene/core/src/test/org/apache/lucene/document/TestLatLonShape.java b/lucene/core/src/test/org/apache/lucene/document/TestLatLonShape.java index 48da9445031..41d52445bc6 100644 --- a/lucene/core/src/test/org/apache/lucene/document/TestLatLonShape.java +++ b/lucene/core/src/test/org/apache/lucene/document/TestLatLonShape.java @@ -19,6 +19,7 @@ package org.apache.lucene.document; import com.carrotsearch.randomizedtesting.generators.RandomNumbers; import org.apache.lucene.document.ShapeField.QueryRelation; import org.apache.lucene.geo.Component2D; +import org.apache.lucene.geo.GeoEncodingUtils; import org.apache.lucene.geo.GeoTestUtil; import org.apache.lucene.geo.Line; import org.apache.lucene.geo.Line2D; @@ -699,4 +700,45 @@ public class TestLatLonShape extends LuceneTestCase { IOUtils.close(w, reader, dir); } + + public void testIndexAndQuerySamePolygon() throws Exception { + Directory dir = newDirectory(); + RandomIndexWriter w = new RandomIndexWriter(random(), dir); + Document doc = new Document(); + Polygon polygon; + while(true) { + try { + polygon = GeoTestUtil.nextPolygon(); + // quantize the polygon + double[] lats = new double[polygon.numPoints()]; + double[] lons = new double[polygon.numPoints()]; + for (int i = 0; i < polygon.numPoints(); i++) { + lats[i] = GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(polygon.getPolyLat(i))); + lons[i] = GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(polygon.getPolyLon(i))); + } + polygon = new Polygon(lats, lons); + Tessellator.tessellate(polygon); + break; + } catch (Exception e) { + // invalid polygon, try a new one + } + } + addPolygonsToDoc(FIELDNAME, doc, polygon); + w.addDocument(doc); + w.forceMerge(1); + + ///// search ////// + IndexReader reader = w.getReader(); + w.close(); + IndexSearcher searcher = newSearcher(reader); + + Query q = LatLonShape.newPolygonQuery(FIELDNAME, QueryRelation.WITHIN, polygon); + assertEquals(1, searcher.count(q)); + q = LatLonShape.newPolygonQuery(FIELDNAME, QueryRelation.INTERSECTS, polygon); + assertEquals(1, searcher.count(q)); + q = LatLonShape.newPolygonQuery(FIELDNAME, QueryRelation.DISJOINT, polygon); + assertEquals(0, searcher.count(q)); + + IOUtils.close(w, reader, dir); + } } diff --git a/lucene/core/src/test/org/apache/lucene/geo/TestPolygon2D.java b/lucene/core/src/test/org/apache/lucene/geo/TestPolygon2D.java index 61cd2b51f33..0206623c950 100644 --- a/lucene/core/src/test/org/apache/lucene/geo/TestPolygon2D.java +++ b/lucene/core/src/test/org/apache/lucene/geo/TestPolygon2D.java @@ -271,6 +271,23 @@ public class TestPolygon2D extends LuceneTestCase { assertTrue(poly.contains(-2, 0)); // left side: true assertTrue(poly.contains(-2, 1)); // left side: true } + + /** Tests edge case behavior with respect to insideness */ + public void testIntersectsSameEdge() { + Component2D poly = Polygon2D.create(new Polygon(new double[] { -2, -2, 2, 2, -2 }, new double[] { -2, 2, 2, -2, -2 })); + // line inside edge + assertEquals(Relation.CELL_INSIDE_QUERY, poly.relateTriangle(-1, -1, 1, 1, -1, -1)); + assertEquals(Relation.CELL_INSIDE_QUERY, poly.relateTriangle(-2, -2, 2, 2, -2, -2)); + // line over edge + assertEquals(Relation.CELL_CROSSES_QUERY, poly.relateTriangle(-4, -4, 4, 4, -4, -4)); + assertEquals(Relation.CELL_CROSSES_QUERY, poly.relateTriangle(-2, -2, 4, 4, 4, 4)); + // line inside edge + assertEquals(Relation.CELL_CROSSES_QUERY, poly.relateTriangle(-1, -1, 3, 3, 1, 1)); + assertEquals(Relation.CELL_CROSSES_QUERY, poly.relateTriangle(-2, -2, 3, 3, 2, 2)); + // line over edge + assertEquals(Relation.CELL_CROSSES_QUERY, poly.relateTriangle(-4, -4, 7, 7, 4, 4)); + assertEquals(Relation.CELL_CROSSES_QUERY, poly.relateTriangle(-2, -2, 7, 7, 4, 4)); + } /** Tests current impl against original algorithm */ public void testContainsAgainstOriginal() {