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-10604: Improve ability to test and debug triangulation algorithm in Tessellator.
(Craig Taverner)
======================= Lucene 9.2.0 =======================

View File

@ -86,6 +86,11 @@ public final class Tessellator {
private Tessellator() {}
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
// this will correct);
// then filter instances of intersections.
@ -131,16 +136,24 @@ public final class Tessellator {
}
// Calculate the tessellation using the doubly LinkedList.
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) {
notifyMonitor(Monitor.FAILED, monitor, null, result);
throw new IllegalArgumentException(
"Unable to Tessellate shape. Possible malformed shape detected.");
}
notifyMonitor(Monitor.COMPLETED, monitor, null, result);
return result;
}
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
// this will correct);
// then filter instances of intersections.0
@ -186,11 +199,14 @@ public final class Tessellator {
}
// Calculate the tessellation using the doubly LinkedList.
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) {
notifyMonitor(Monitor.FAILED, monitor, null, result);
throw new IllegalArgumentException(
"Unable to Tessellate shape. Possible malformed shape detected.");
}
notifyMonitor(Monitor.COMPLETED, monitor, null, result);
return result;
}
@ -512,7 +528,9 @@ public final class Tessellator {
Node currEar,
final List<Triangle> tessellation,
State state,
final boolean mortonOptimized) {
final boolean mortonOptimized,
final Monitor monitor,
int depth) {
earcut:
do {
if (currEar == null || currEar.previous == currEar.next) {
@ -525,6 +543,7 @@ public final class Tessellator {
// Iteratively slice ears
do {
notifyMonitor(state, depth, monitor, currEar, tessellation);
prevNode = currEar.previous;
nextNode = currEar.next;
// Determine whether the current triangle must be cut off.
@ -570,8 +589,10 @@ public final class Tessellator {
continue earcut;
case SPLIT:
// 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
notifyMonitor(state.name() + "[FAILED]", monitor, currEar, tessellation);
throw new IllegalArgumentException(
"Unable to Tessellate shape. Possible malformed shape detected.");
}
@ -797,7 +818,9 @@ public final class Tessellator {
final Object polygon,
final Node start,
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.
Node searchNode = start;
do {
@ -817,8 +840,12 @@ public final class Tessellator {
sortByMortonWithReset(searchNode);
sortByMortonWithReset(splitNode);
}
earcutLinkedList(polygon, searchNode, tessellation, State.INIT, mortonOptimized);
earcutLinkedList(polygon, splitNode, tessellation, State.INIT, mortonOptimized);
notifyMonitorSplit(depth, monitor, searchNode, splitNode);
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
return true;
}
@ -1444,6 +1471,71 @@ public final class Tessellator {
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 */
protected static class Node {
// 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.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import java.text.ParseException;
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 {
String geoJson = GeoTestUtil.readShape("lucene-10563-2.geojson.gz");
Polygon[] polygons = Polygon.fromGeoJSON(geoJson);
@ -877,6 +891,28 @@ public class TestTessellator extends LuceneTestCase {
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 {
Polygon polygon = (Polygon) SimpleWKTShapeParser.parse(wkt);
List<Tessellator.Triangle> tessellation =