diff --git a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayUtil.java b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayUtil.java index 4478a06ab1..cab5296a9c 100644 --- a/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayUtil.java +++ b/modules/core/src/main/java/org/locationtech/jts/operation/overlayng/OverlayUtil.java @@ -407,7 +407,7 @@ public static boolean isResultAreaConsistent(Geometry geom0, Geometry geom1, int isConsistent = isDifferenceAreaConsistent(areaA, areaB, areaResult, AREA_HEURISTIC_TOLERANCE); break; case OverlayNG.SYMDIFFERENCE: - isConsistent = isLess(areaResult, areaA + areaB, AREA_HEURISTIC_TOLERANCE); + isConsistent = isSymDifferenceAreaConsistent(areaA, areaB, areaResult, AREA_HEURISTIC_TOLERANCE); break; case OverlayNG.UNION: isConsistent = isLess(areaA, areaResult, AREA_HEURISTIC_TOLERANCE) @@ -435,6 +435,30 @@ private static boolean isDifferenceAreaConsistent(double areaA, double areaB, do return areaResult > areaDiffMin; } + /** + * Tests if the area of a symmetric difference is within the possible range. + * It cannot be greater than the sum of the input areas, + * and it cannot be less than the difference of the input areas. + * The lower bound is relaxed by a fraction of the larger input area, + * so that nearly identical inputs (whose areas differ only by rounding) + * can still produce an empty result. + * This is a heuristic which will only detect gross overlay errors + * (such as an inverted result with negative area). + * + * @param areaA the area of A + * @param areaB the area of B + * @param areaResult the result area + * @param tolFrac the area tolerance fraction + * + * @return true if the symmetric difference area is consistent. + */ + private static boolean isSymDifferenceAreaConsistent(double areaA, double areaB, double areaResult, double tolFrac) { + if (! isLess(areaResult, areaA + areaB, tolFrac)) + return false; + double areaSymDiffMin = Math.abs(areaA - areaB) - tolFrac * Math.max(areaA, areaB); + return areaResult >= areaSymDiffMin; + } + private static boolean isLess(double v1, double v2, double tol) { return v1 <= v2 * (1 + tol); } diff --git a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGRobustTest.java b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGRobustTest.java index 1c2d10157e..15f4218b37 100644 --- a/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGRobustTest.java +++ b/modules/core/src/test/java/org/locationtech/jts/operation/overlayng/OverlayNGRobustTest.java @@ -80,6 +80,22 @@ public void testUnionFixed() { OverlayNG.overlay(geom1, geom2, OverlayNG.UNION, pm); } + /** + * The first snapping attempt for this input produces an inverted polygon + * (with negative area), which the area check must reject for a symmetric difference. + * The retry with a larger snap tolerance gives the correct result, + * which is the polygon, since the line has no area. + * + * See https://github.com/locationtech/jts/issues/1239 + */ + public void testSymDifferencePolygonLineInvertedResult() { + Geometry a = read("POLYGON ((20.58 46.965, 27.52 53.905, 28.52 54.914, 20.58 46.965))"); + Geometry b = read("LINESTRING (27.519999973297118 53.90499997329712, 52.192 78.577, 27.519 53.904, 28.519999999 54.913999999, 39.599 65.984)"); + Geometry result = OverlayNGRobust.overlay(a, b, OverlayNG.SYMDIFFERENCE); + assertTrue("result is valid", result.isValid()); + assertEquals(a.getArea(), result.getArea(), 1e-6); + } + // MD 2020-09-14 There is no known test case that requires Snap-Rounding to succeed. public static void checkUnionSuccess(Geometry a, Geometry b) {