Skip to content

Commit

Permalink
LUCENE-9538: Detect polygon self-intersections in the Tessellator (#428)
Browse files Browse the repository at this point in the history
Detect self-intersections so it can provide a more meaningful error to the users.
  • Loading branch information
iverase committed Nov 29, 2021
1 parent 62084d7 commit 70243ea
Show file tree
Hide file tree
Showing 12 changed files with 257 additions and 73 deletions.
2 changes: 2 additions & 0 deletions lucene/CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ Improvements
* LUCENE-10262: Lift up restrictions for navigating PointValues#PointTree
added in LUCENE-9820 (Ignacio Vera)

* LUCENE-9538: Detect polygon self-intersections in the Tessellator. (Ignacio Vera)

Optimizations
---------------------

Expand Down
25 changes: 18 additions & 7 deletions lucene/core/src/java/org/apache/lucene/document/LatLonShape.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;

import java.util.ArrayList;
import java.util.List;
import org.apache.lucene.document.ShapeField.QueryRelation; // javadoc
import org.apache.lucene.document.ShapeField.Triangle;
Expand All @@ -45,6 +44,8 @@
*
* <ul>
* <li>{@link #createIndexableFields(String, Polygon)} for indexing a geo polygon.
* <li>{@link #createIndexableFields(String, Polygon, boolean)} for indexing a geo polygon with
* the possibility of checking for self-intersections.
* <li>{@link #createIndexableFields(String, Line)} for indexing a geo linestring.
* <li>{@link #createIndexableFields(String, double, double)} for indexing a lat, lon geo point.
* <li>{@link #newBoxQuery newBoxQuery()} for matching geo shapes that have some {@link
Expand All @@ -69,15 +70,25 @@ public class LatLonShape {
// no instance:
private LatLonShape() {}

/** create indexable fields for polygon geometry */
/** create indexable fields for polygon geometry. */
public static Field[] createIndexableFields(String fieldName, Polygon polygon) {
return createIndexableFields(fieldName, polygon, false);
}

/**
* create indexable fields for polygon geometry. If {@code checkSelfIntersections} is set to true,
* the validity of the provided polygon is checked with a small performance penalty.
*/
public static Field[] createIndexableFields(
String fieldName, Polygon polygon, boolean checkSelfIntersections) {
// the lionshare of the indexing is done by the tessellator
List<Tessellator.Triangle> tessellation = Tessellator.tessellate(polygon);
List<Triangle> fields = new ArrayList<>();
for (Tessellator.Triangle t : tessellation) {
fields.add(new Triangle(fieldName, t));
List<Tessellator.Triangle> tessellation =
Tessellator.tessellate(polygon, checkSelfIntersections);
Triangle[] fields = new Triangle[tessellation.size()];
for (int i = 0; i < tessellation.size(); i++) {
fields[i] = new Triangle(fieldName, tessellation.get(i));
}
return fields.toArray(new Field[fields.size()]);
return fields;
}

/** create indexable fields for line geometry */
Expand Down
25 changes: 18 additions & 7 deletions lucene/core/src/java/org/apache/lucene/document/XYShape.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@

import static org.apache.lucene.geo.XYEncodingUtils.encode;

import java.util.ArrayList;
import java.util.List;
import org.apache.lucene.document.ShapeField.QueryRelation; // javadoc
import org.apache.lucene.document.ShapeField.QueryRelation;
import org.apache.lucene.document.ShapeField.Triangle;
import org.apache.lucene.geo.Tessellator;
import org.apache.lucene.geo.XYCircle;
Expand All @@ -43,6 +42,8 @@
*
* <ul>
* <li>{@link #createIndexableFields(String, XYPolygon)} for indexing a cartesian polygon.
* <li>{@link #createIndexableFields(String, XYPolygon, boolean)} for indexing a cartesian polygon
* with the possibility of checking for self-intersections.
* <li>{@link #createIndexableFields(String, XYLine)} for indexing a cartesian linestring.
* <li>{@link #createIndexableFields(String, float, float)} for indexing a x, y cartesian point.
* <li>{@link #newBoxQuery newBoxQuery()} for matching cartesian shapes that have some {@link
Expand All @@ -68,13 +69,23 @@ private XYShape() {}

/** create indexable fields for cartesian polygon geometry */
public static Field[] createIndexableFields(String fieldName, XYPolygon polygon) {
return createIndexableFields(fieldName, polygon, false);
}

List<Tessellator.Triangle> tessellation = Tessellator.tessellate(polygon);
List<Triangle> fields = new ArrayList<>(tessellation.size());
for (Tessellator.Triangle t : tessellation) {
fields.add(new Triangle(fieldName, t));
/**
* create indexable fields for cartesian polygon geometry. If {@code checkSelfIntersections} is
* set to true, the validity of the provided polygon is checked with a small performance penalty.
*/
public static Field[] createIndexableFields(
String fieldName, XYPolygon polygon, boolean checkSelfIntersections) {

List<Tessellator.Triangle> tessellation =
Tessellator.tessellate(polygon, checkSelfIntersections);
Triangle[] fields = new Triangle[tessellation.size()];
for (int i = 0; i < tessellation.size(); i++) {
fields[i] = new Triangle(fieldName, tessellation.get(i));
}
return fields.toArray(new Field[fields.size()]);
return fields;
}

/** create indexable fields for cartesian line geometry */
Expand Down
23 changes: 18 additions & 5 deletions lucene/core/src/java/org/apache/lucene/geo/GeoUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,24 @@ public static boolean lineCrossesLine(
double a2y,
double b2x,
double b2y) {
if (orient(a2x, a2y, b2x, b2y, a1x, a1y) * orient(a2x, a2y, b2x, b2y, b1x, b1y) < 0
&& orient(a1x, a1y, b1x, b1y, a2x, a2y) * orient(a1x, a1y, b1x, b1y, b2x, b2y) < 0) {
return true;
}
return false;
return orient(a2x, a2y, b2x, b2y, a1x, a1y) * orient(a2x, a2y, b2x, b2y, b1x, b1y) < 0
&& orient(a1x, a1y, b1x, b1y, a2x, a2y) * orient(a1x, a1y, b1x, b1y, b2x, b2y) < 0;
}

/** uses orient method to compute whether two line overlap each other */
public static boolean lineOverlapLine(
double a1x,
double a1y,
double b1x,
double b1y,
double a2x,
double a2y,
double b2x,
double b2y) {
return orient(a2x, a2y, b2x, b2y, a1x, a1y) == 0
&& orient(a2x, a2y, b2x, b2y, b1x, b1y) == 0
&& orient(a1x, a1y, b1x, b1y, a2x, a2y) == 0
&& orient(a1x, a1y, b1x, b1y, b2x, b2y) == 0;
}

/**
Expand Down
135 changes: 128 additions & 7 deletions lucene/core/src/java/org/apache/lucene/geo/Tessellator.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import static org.apache.lucene.geo.GeoEncodingUtils.encodeLatitude;
import static org.apache.lucene.geo.GeoEncodingUtils.encodeLongitude;
import static org.apache.lucene.geo.GeoUtils.lineCrossesLine;
import static org.apache.lucene.geo.GeoUtils.lineOverlapLine;
import static org.apache.lucene.geo.GeoUtils.orient;

import java.util.ArrayList;
Expand Down Expand Up @@ -83,7 +85,7 @@ private enum State {
// No Instance:
private Tessellator() {}

public static final List<Triangle> tessellate(final Polygon polygon) {
public static List<Triangle> tessellate(final Polygon polygon, boolean checkSelfIntersections) {
// Attempt to establish a doubly-linked list of the provided shell points (should be CCW, but
// this will correct);
// then filter instances of intersections.
Expand Down Expand Up @@ -121,18 +123,21 @@ public static final List<Triangle> tessellate(final Polygon polygon) {
sortByMorton(outerNode);
}
}
if (checkSelfIntersections) {
checkIntersection(outerNode, mortonOptimized);
}
// Calculate the tessellation using the doubly LinkedList.
List<Triangle> result =
earcutLinkedList(polygon, outerNode, new ArrayList<>(), State.INIT, mortonOptimized);
if (result.size() == 0) {
throw new IllegalArgumentException(
"Unable to Tessellate shape [" + polygon + "]. Possible malformed shape detected.");
"Unable to Tessellate shape. Possible malformed shape detected.");
}

return result;
}

public static final List<Triangle> tessellate(final XYPolygon polygon) {
public static List<Triangle> tessellate(final XYPolygon polygon, boolean checkSelfIntersections) {
// Attempt to establish a doubly-linked list of the provided shell points (should be CCW, but
// this will correct);
// then filter instances of intersections.0
Expand Down Expand Up @@ -170,12 +175,15 @@ public static final List<Triangle> tessellate(final XYPolygon polygon) {
sortByMorton(outerNode);
}
}
if (checkSelfIntersections) {
checkIntersection(outerNode, mortonOptimized);
}
// Calculate the tessellation using the doubly LinkedList.
List<Triangle> result =
earcutLinkedList(polygon, outerNode, new ArrayList<>(), State.INIT, mortonOptimized);
if (result.size() == 0) {
throw new IllegalArgumentException(
"Unable to Tessellate shape [" + polygon + "]. Possible malformed shape detected.");
"Unable to Tessellate shape. Possible malformed shape detected.");
}

return result;
Expand Down Expand Up @@ -559,9 +567,7 @@ private static final List<Triangle> earcutLinkedList(
if (splitEarcut(polygon, currEar, tessellation, mortonOptimized) == false) {
// we could not process all points. Tessellation failed
throw new IllegalArgumentException(
"Unable to Tessellate shape ["
+ polygon
+ "]. Possible malformed shape detected.");
"Unable to Tessellate shape. Possible malformed shape detected.");
}
break;
}
Expand Down Expand Up @@ -818,6 +824,121 @@ private static final boolean splitEarcut(
return false;
}

/** Computes if edge defined by a and b overlaps with a polygon edge * */
private static void checkIntersection(Node a, boolean isMorton) {
Node next = a.next;
do {
Node innerNext = next.next;
if (isMorton) {
mortonCheckIntersection(next, innerNext);
} else {
do {
checkIntersectionPoint(next, innerNext);
innerNext = innerNext.next;
} while (innerNext != next.previous);
}
next = next.next;
} while (next != a.previous);
}

/**
* Uses morton code for speed to determine whether or not and edge defined by a and b overlaps
* with a polygon edge
*/
private static final void mortonCheckIntersection(final Node a, final Node b) {
// edge bbox (flip the bits so negative encoded values are < positive encoded values)
int minTX = StrictMath.min(a.x, a.next.x) ^ 0x80000000;
int minTY = StrictMath.min(a.y, a.next.y) ^ 0x80000000;
int maxTX = StrictMath.max(a.x, a.next.x) ^ 0x80000000;
int maxTY = StrictMath.max(a.y, a.next.y) ^ 0x80000000;

// z-order range for the current edge;
long minZ = BitUtil.interleave(minTX, minTY);
long maxZ = BitUtil.interleave(maxTX, maxTY);

// now make sure we don't have other points inside the potential ear;

// look for points inside edge in both directions
Node p = b.previousZ;
Node n = b.nextZ;
while (p != null
&& Long.compareUnsigned(p.morton, minZ) >= 0
&& n != null
&& Long.compareUnsigned(n.morton, maxZ) <= 0) {
checkIntersectionPoint(p, a);
p = p.previousZ;
checkIntersectionPoint(n, a);
n = n.nextZ;
}

// first look for points inside the edge in decreasing z-order
while (p != null && Long.compareUnsigned(p.morton, minZ) >= 0) {
checkIntersectionPoint(p, a);
p = p.previousZ;
}
// then look for points in increasing z-order
while (n != null && Long.compareUnsigned(n.morton, maxZ) <= 0) {
checkIntersectionPoint(n, a);
n = n.nextZ;
}
}

private static void checkIntersectionPoint(final Node a, final Node b) {
if (a == b) {
return;
}

if (Math.max(a.getY(), a.next.getY()) <= Math.min(b.getY(), b.next.getY())
|| Math.min(a.getY(), a.next.getY()) >= Math.max(b.getY(), b.next.getY())
|| Math.max(a.getX(), a.next.getX()) <= Math.min(b.getX(), b.next.getX())
|| Math.min(a.getX(), a.next.getX()) >= Math.max(b.getX(), b.next.getX())) {
return;
}

if (lineCrossesLine(
a.getX(),
a.getY(),
a.next.getX(),
a.next.getY(),
b.getX(),
b.getY(),
b.next.getX(),
b.next.getY())) {
// Line AB represented as a1x + b1y = c1
double a1 = a.next.getY() - a.getY();
double b1 = a.getX() - a.next.getX();
double c1 = a1 * (a.getX()) + b1 * (a.getY());

// Line CD represented as a2x + b2y = c2
double a2 = b.next.getY() - b.getY();
double b2 = b.getX() - b.next.getX();
double c2 = a2 * (b.getX()) + b2 * (b.getY());

double determinant = a1 * b2 - a2 * b1;

assert determinant != 0;

double x = (b2 * c1 - b1 * c2) / determinant;
double y = (a1 * c2 - a2 * c1) / determinant;

throw new IllegalArgumentException("Polygon self-intersection at lat=" + y + " lon=" + x);
}
if (a.isNextEdgeFromPolygon
&& b.isNextEdgeFromPolygon
&& lineOverlapLine(
a.getX(),
a.getY(),
a.next.getX(),
a.next.getY(),
b.getX(),
b.getY(),
b.next.getX(),
b.next.getY())) {
throw new IllegalArgumentException(
"Polygon ring self-intersection at lat=" + a.getY() + " lon=" + a.getX());
}
}

/** Computes if edge defined by a and b overlaps with a polygon edge * */
private static boolean isEdgeFromPolygon(final Node a, final Node b, final boolean isMorton) {
if (isMorton) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ public Polygon nextShape() {
while (true) {
Polygon p = GeoTestUtil.nextPolygon();
try {
Tessellator.tessellate(p);
Tessellator.tessellate(p, random().nextBoolean());
return p;
} catch (
@SuppressWarnings("unused")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ public XYPolygon nextShape() {
while (true) {
XYPolygon p = ShapeTestUtil.nextPolygon();
try {
Tessellator.tessellate(p);
Tessellator.tessellate(p, random().nextBoolean());
return p;
} catch (
@SuppressWarnings("unused")
Expand Down
Loading

0 comments on commit 70243ea

Please sign in to comment.