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-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 =======================
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
Loading…
Reference in New Issue