mirror of https://github.com/apache/lucene.git
LUCENE-10604: Add Tessellation monitor for easier debugging of triangulation algorithm (#946)
This commit is contained in:
parent
26f21ae36d
commit
66b65b79e8
|
@ -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 =======================
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 =
|
||||
|
|
Loading…
Reference in New Issue