LUCENE-10604: Add Tessellation monitor for easier debugging of triangulation algorithm (#946)

This commit is contained in:
Craig Taverner 2022-06-10 07:33:07 +02:00 committed by GitHub
parent 26f21ae36d
commit 66b65b79e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 137 additions and 7 deletions

View File

@ -106,6 +106,8 @@ Other
* LUCENE-10370: pass proper classpath/module arguments for forking jvms from within tests. (Dawid Weiss) * LUCENE-10370: pass proper classpath/module arguments for forking jvms from within tests. (Dawid Weiss)
* LUCENE-10604: Improve ability to test and debug triangulation algorithm in Tessellator.
(Craig Taverner)
======================= Lucene 9.2.0 ======================= ======================= Lucene 9.2.0 =======================

View File

@ -86,6 +86,11 @@ public final class Tessellator {
private Tessellator() {} private Tessellator() {}
public static List<Triangle> tessellate(final Polygon polygon, boolean checkSelfIntersections) { public static List<Triangle> tessellate(final Polygon polygon, boolean checkSelfIntersections) {
return tessellate(polygon, checkSelfIntersections, null);
}
public static List<Triangle> tessellate(
final Polygon polygon, boolean checkSelfIntersections, Monitor monitor) {
// Attempt to establish a doubly-linked list of the provided shell points (should be CCW, but // Attempt to establish a doubly-linked list of the provided shell points (should be CCW, but
// this will correct); // this will correct);
// then filter instances of intersections. // then filter instances of intersections.
@ -131,16 +136,24 @@ public final class Tessellator {
} }
// Calculate the tessellation using the doubly LinkedList. // Calculate the tessellation using the doubly LinkedList.
List<Triangle> result = List<Triangle> result =
earcutLinkedList(polygon, outerNode, new ArrayList<>(), State.INIT, mortonOptimized); earcutLinkedList(
polygon, outerNode, new ArrayList<>(), State.INIT, mortonOptimized, monitor, 0);
if (result.size() == 0) { if (result.size() == 0) {
notifyMonitor(Monitor.FAILED, monitor, null, result);
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Unable to Tessellate shape. Possible malformed shape detected."); "Unable to Tessellate shape. Possible malformed shape detected.");
} }
notifyMonitor(Monitor.COMPLETED, monitor, null, result);
return result; return result;
} }
public static List<Triangle> tessellate(final XYPolygon polygon, boolean checkSelfIntersections) { public static List<Triangle> tessellate(final XYPolygon polygon, boolean checkSelfIntersections) {
return tessellate(polygon, checkSelfIntersections, null);
}
public static List<Triangle> tessellate(
final XYPolygon polygon, boolean checkSelfIntersections, Monitor monitor) {
// Attempt to establish a doubly-linked list of the provided shell points (should be CCW, but // Attempt to establish a doubly-linked list of the provided shell points (should be CCW, but
// this will correct); // this will correct);
// then filter instances of intersections.0 // then filter instances of intersections.0
@ -186,11 +199,14 @@ public final class Tessellator {
} }
// Calculate the tessellation using the doubly LinkedList. // Calculate the tessellation using the doubly LinkedList.
List<Triangle> result = List<Triangle> result =
earcutLinkedList(polygon, outerNode, new ArrayList<>(), State.INIT, mortonOptimized); earcutLinkedList(
polygon, outerNode, new ArrayList<>(), State.INIT, mortonOptimized, monitor, 0);
if (result.size() == 0) { if (result.size() == 0) {
notifyMonitor(Monitor.FAILED, monitor, null, result);
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Unable to Tessellate shape. Possible malformed shape detected."); "Unable to Tessellate shape. Possible malformed shape detected.");
} }
notifyMonitor(Monitor.COMPLETED, monitor, null, result);
return result; return result;
} }
@ -512,7 +528,9 @@ public final class Tessellator {
Node currEar, Node currEar,
final List<Triangle> tessellation, final List<Triangle> tessellation,
State state, State state,
final boolean mortonOptimized) { final boolean mortonOptimized,
final Monitor monitor,
int depth) {
earcut: earcut:
do { do {
if (currEar == null || currEar.previous == currEar.next) { if (currEar == null || currEar.previous == currEar.next) {
@ -525,6 +543,7 @@ public final class Tessellator {
// Iteratively slice ears // Iteratively slice ears
do { do {
notifyMonitor(state, depth, monitor, currEar, tessellation);
prevNode = currEar.previous; prevNode = currEar.previous;
nextNode = currEar.next; nextNode = currEar.next;
// Determine whether the current triangle must be cut off. // Determine whether the current triangle must be cut off.
@ -570,8 +589,10 @@ public final class Tessellator {
continue earcut; continue earcut;
case SPLIT: case SPLIT:
// as a last resort, try splitting the remaining polygon into two // as a last resort, try splitting the remaining polygon into two
if (splitEarcut(polygon, currEar, tessellation, mortonOptimized) == false) { if (splitEarcut(polygon, currEar, tessellation, mortonOptimized, monitor, depth + 1)
== false) {
// we could not process all points. Tessellation failed // we could not process all points. Tessellation failed
notifyMonitor(state.name() + "[FAILED]", monitor, currEar, tessellation);
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Unable to Tessellate shape. Possible malformed shape detected."); "Unable to Tessellate shape. Possible malformed shape detected.");
} }
@ -797,7 +818,9 @@ public final class Tessellator {
final Object polygon, final Object polygon,
final Node start, final Node start,
final List<Triangle> tessellation, final List<Triangle> tessellation,
final boolean mortonOptimized) { final boolean mortonOptimized,
final Monitor monitor,
int depth) {
// Search for a valid diagonal that divides the polygon into two. // Search for a valid diagonal that divides the polygon into two.
Node searchNode = start; Node searchNode = start;
do { do {
@ -817,8 +840,12 @@ public final class Tessellator {
sortByMortonWithReset(searchNode); sortByMortonWithReset(searchNode);
sortByMortonWithReset(splitNode); sortByMortonWithReset(splitNode);
} }
earcutLinkedList(polygon, searchNode, tessellation, State.INIT, mortonOptimized); notifyMonitorSplit(depth, monitor, searchNode, splitNode);
earcutLinkedList(polygon, splitNode, tessellation, State.INIT, mortonOptimized); earcutLinkedList(
polygon, searchNode, tessellation, State.INIT, mortonOptimized, monitor, depth);
earcutLinkedList(
polygon, splitNode, tessellation, State.INIT, mortonOptimized, monitor, depth);
notifyMonitorSplitEnd(depth, monitor);
// Finish the iterative search // Finish the iterative search
return true; return true;
} }
@ -1444,6 +1471,71 @@ public final class Tessellator {
return false; return false;
} }
/**
* Implementation of this interface will receive calls with internal data at each step of the
* triangulation algorithm. This is of use for debugging complex cases, as well as gaining insight
* into the way the algorithm works. Data provided includes a status string containing the current
* mode, list of points representing the current linked-list of internal nodes used for
* triangulation, and a list of triangles so far created by the algorithm.
*/
public interface Monitor {
String FAILED = "FAILED";
String COMPLETED = "COMPLETED";
/** Each loop of the main earclip algorithm will call this with the current state */
void currentState(String status, List<Point> points, List<Triangle> tessellation);
/** When a new polygon split is entered for mode=SPLIT, this is called. */
void startSplit(String status, List<Point> leftPolygon, List<Point> rightPolygon);
/** When a polygon split is completed, this is called. */
void endSplit(String status);
}
private static List<Point> getPoints(Node start) {
Node node = start;
ArrayList<Point> points = new ArrayList<>();
do {
points.add(new Point(node.getY(), node.getX()));
node = node.next;
} while (node != start);
return points;
}
private static void notifyMonitorSplit(
int depth, Monitor monitor, Node searchNode, Node diagonalNode) {
if (monitor != null) {
if (searchNode == null || diagonalNode == null)
throw new IllegalStateException("Invalid split provided to monitor");
monitor.startSplit("SPLIT[" + depth + "]", getPoints(searchNode), getPoints(diagonalNode));
}
}
private static void notifyMonitorSplitEnd(int depth, Monitor monitor) {
if (monitor != null) {
monitor.endSplit("SPLIT[" + depth + "]");
}
}
private static void notifyMonitor(
State state, int depth, Monitor monitor, Node start, List<Triangle> tessellation) {
if (monitor != null) {
notifyMonitor(
state.name() + (depth == 0 ? "" : "[" + depth + "]"), monitor, start, tessellation);
}
}
private static void notifyMonitor(
String status, Monitor monitor, Node start, List<Triangle> tessellation) {
if (monitor != null) {
if (start == null) {
monitor.currentState(status, null, tessellation);
} else {
monitor.currentState(status, getPoints(start), tessellation);
}
}
}
/** Circular Doubly-linked list used for polygon coordinates */ /** Circular Doubly-linked list used for polygon coordinates */
protected static class Node { protected static class Node {
// node index in the linked list // node index in the linked list

View File

@ -18,6 +18,7 @@ package org.apache.lucene.geo;
import static org.apache.lucene.tests.geo.GeoTestUtil.nextBoxNotCrossingDateline; import static org.apache.lucene.tests.geo.GeoTestUtil.nextBoxNotCrossingDateline;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import java.text.ParseException; import java.text.ParseException;
import java.util.List; import java.util.List;
@ -837,6 +838,19 @@ public class TestTessellator extends LuceneTestCase {
} }
} }
public void testComplexPolygon50_WithMonitor() throws Exception {
String geoJson = GeoTestUtil.readShape("lucene-10563-1.geojson.gz");
Polygon[] polygons = Polygon.fromGeoJSON(geoJson);
assertThat("Only one polygon", polygons.length, equalTo(1));
Polygon polygon = polygons[0];
TestCountingMonitor monitor = new TestCountingMonitor();
Tessellator.tessellate(polygon, true, monitor);
assertThat("Expected many monitor calls", monitor.count, greaterThan(400));
assertThat("Expected specific number of splits", monitor.splitsStarted, equalTo(3));
assertThat(
"Expected splits to start and end", monitor.splitsStarted, equalTo(monitor.splitsEnded));
}
public void testComplexPolygon51() throws Exception { public void testComplexPolygon51() throws Exception {
String geoJson = GeoTestUtil.readShape("lucene-10563-2.geojson.gz"); String geoJson = GeoTestUtil.readShape("lucene-10563-2.geojson.gz");
Polygon[] polygons = Polygon.fromGeoJSON(geoJson); Polygon[] polygons = Polygon.fromGeoJSON(geoJson);
@ -877,6 +891,28 @@ public class TestTessellator extends LuceneTestCase {
ex.getMessage()); ex.getMessage());
} }
private static class TestCountingMonitor implements Tessellator.Monitor {
private int count = 0;
private int splitsStarted = 0;
private int splitsEnded = 0;
@Override
public void currentState(
String status, List<Point> points, List<Tessellator.Triangle> tessellation) {
count++;
}
@Override
public void startSplit(String status, List<Point> leftPolygon, List<Point> rightPolygon) {
splitsStarted++;
}
@Override
public void endSplit(String status) {
splitsEnded++;
}
}
private void checkPolygon(String wkt) throws Exception { private void checkPolygon(String wkt) throws Exception {
Polygon polygon = (Polygon) SimpleWKTShapeParser.parse(wkt); Polygon polygon = (Polygon) SimpleWKTShapeParser.parse(wkt);
List<Tessellator.Triangle> tessellation = List<Tessellator.Triangle> tessellation =