mirror of https://github.com/apache/lucene.git
LUCENE-9152: Improve line intersection detection for polygons (#1187)
This commit is contained in:
parent
47c01af394
commit
29542c7f59
|
@ -53,6 +53,8 @@ Improvements
|
||||||
|
|
||||||
* LUCENE-4702: Better compression of terms dictionaries. (Adrien Grand)
|
* 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
|
Optimizations
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
|
|
@ -261,7 +261,7 @@ public class EdgeTree {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns true if the line crosses any edge in this edge subtree */
|
/** 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) {
|
if (minY <= max) {
|
||||||
double a1x = x1;
|
double a1x = x1;
|
||||||
double a1y = y1;
|
double a1y = y1;
|
||||||
|
@ -272,14 +272,21 @@ public class EdgeTree {
|
||||||
(a1y > maxY && b1y > maxY) ||
|
(a1y > maxY && b1y > maxY) ||
|
||||||
(a1x < minX && b1x < minX) ||
|
(a1x < minX && b1x < minX) ||
|
||||||
(a1x > maxX && b1x > maxX);
|
(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;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (right != null && maxY >= low && right.crossesLine(minX, maxX, minY, maxY, a2x, a2y, b2x, b2y, includeBoundary)) {
|
||||||
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)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,19 +109,19 @@ public final class Line2D implements Component2D {
|
||||||
}
|
}
|
||||||
} else if (ax == cx && ay == cy) {
|
} else if (ax == cx && ay == cy) {
|
||||||
// indexed "triangle" is a line:
|
// 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_CROSSES_QUERY;
|
||||||
}
|
}
|
||||||
return Relation.CELL_OUTSIDE_QUERY;
|
return Relation.CELL_OUTSIDE_QUERY;
|
||||||
} else if (ax == bx && ay == by) {
|
} else if (ax == bx && ay == by) {
|
||||||
// indexed "triangle" is a line:
|
// 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_CROSSES_QUERY;
|
||||||
}
|
}
|
||||||
return Relation.CELL_OUTSIDE_QUERY;
|
return Relation.CELL_OUTSIDE_QUERY;
|
||||||
} else if (bx == cx && by == cy) {
|
} else if (bx == cx && by == cy) {
|
||||||
// indexed "triangle" is a line:
|
// 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_CROSSES_QUERY;
|
||||||
}
|
}
|
||||||
return Relation.CELL_OUTSIDE_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 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
|
// 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
|
// 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) {
|
if (ab == true) {
|
||||||
return WithinRelation.NOTWITHIN;
|
return WithinRelation.NOTWITHIN;
|
||||||
} else {
|
} 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) {
|
if (bc == true) {
|
||||||
return WithinRelation.NOTWITHIN;
|
return WithinRelation.NOTWITHIN;
|
||||||
} else {
|
} else {
|
||||||
relation = WithinRelation.CANDIDATE;
|
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) {
|
if (ca == true) {
|
||||||
return WithinRelation.NOTWITHIN;
|
return WithinRelation.NOTWITHIN;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -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 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
|
// 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
|
// 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) {
|
if (ab == true) {
|
||||||
return WithinRelation.NOTWITHIN;
|
return WithinRelation.NOTWITHIN;
|
||||||
} else {
|
} 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) {
|
if (bc == true) {
|
||||||
return WithinRelation.NOTWITHIN;
|
return WithinRelation.NOTWITHIN;
|
||||||
} else {
|
} else {
|
||||||
relation = WithinRelation.CANDIDATE;
|
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) {
|
if (ca == true) {
|
||||||
return WithinRelation.NOTWITHIN;
|
return WithinRelation.NOTWITHIN;
|
||||||
} else {
|
} else {
|
||||||
|
@ -236,12 +236,12 @@ public class Polygon2D implements Component2D {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (numCorners == 2) {
|
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_CROSSES_QUERY;
|
||||||
}
|
}
|
||||||
return Relation.CELL_INSIDE_QUERY;
|
return Relation.CELL_INSIDE_QUERY;
|
||||||
} else if (numCorners == 0) {
|
} 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_CROSSES_QUERY;
|
||||||
}
|
}
|
||||||
return Relation.CELL_OUTSIDE_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) {
|
if (Component2D.pointInTriangle(minX, maxX, minY, maxY, tree.x1, tree.y1, ax, ay, bx, by, cx, cy) == true) {
|
||||||
return Relation.CELL_CROSSES_QUERY;
|
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_CROSSES_QUERY;
|
||||||
}
|
}
|
||||||
return Relation.CELL_OUTSIDE_QUERY;
|
return Relation.CELL_OUTSIDE_QUERY;
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.apache.lucene.document;
|
||||||
import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
|
import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
|
||||||
import org.apache.lucene.document.ShapeField.QueryRelation;
|
import org.apache.lucene.document.ShapeField.QueryRelation;
|
||||||
import org.apache.lucene.geo.Component2D;
|
import org.apache.lucene.geo.Component2D;
|
||||||
|
import org.apache.lucene.geo.GeoEncodingUtils;
|
||||||
import org.apache.lucene.geo.GeoTestUtil;
|
import org.apache.lucene.geo.GeoTestUtil;
|
||||||
import org.apache.lucene.geo.Line;
|
import org.apache.lucene.geo.Line;
|
||||||
import org.apache.lucene.geo.Line2D;
|
import org.apache.lucene.geo.Line2D;
|
||||||
|
@ -699,4 +700,45 @@ public class TestLatLonShape extends LuceneTestCase {
|
||||||
|
|
||||||
IOUtils.close(w, reader, dir);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -271,6 +271,23 @@ public class TestPolygon2D extends LuceneTestCase {
|
||||||
assertTrue(poly.contains(-2, 0)); // left side: true
|
assertTrue(poly.contains(-2, 0)); // left side: true
|
||||||
assertTrue(poly.contains(-2, 1)); // 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 */
|
/** Tests current impl against original algorithm */
|
||||||
public void testContainsAgainstOriginal() {
|
public void testContainsAgainstOriginal() {
|
||||||
|
|
Loading…
Reference in New Issue