From b8d4e6e57b3d973bd7fd75bcdcfa0c4af523559d Mon Sep 17 00:00:00 2001 From: adityaanikam Date: Sat, 10 Oct 2026 01:32:30 +0530 Subject: [PATCH] Add lower bound to the OverlayNG area check for symDifference OverlayUtil.isResultAreaConsistent only checked an upper bound for SYMDIFFERENCE, so a result with negative area was accepted. For the input in #1239 this stopped OverlayNGRobust from retrying with a larger snap tolerance, and an inverted polygon was returned. A symmetric difference cannot be smaller than the difference of the input areas. Check that, relaxed by the area tolerance as it is for DIFFERENCE, so that nearly identical inputs still give an empty result. Fixes #1239 Signed-off-by: adityaanikam --- .../jts/operation/overlayng/OverlayUtil.java | 26 ++++++++++++++++++- .../overlayng/OverlayNGRobustTest.java | 16 ++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) 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) {