Closed grimsa closed 1 year ago
Thanks for the detailed bug report. Sorry for the delay in looking into this. It looks like a design flaw in the heuristic area check.
In this case the result is an empty geometry with zero area. The input geometries are essentially identical, but have rotated vertex sequences. Due to numeric precision issues (the eternal bugbear) the areas evaluate to be slightly different. This causes the area heuristic check to fail, incorrectly.
A : MULTIPOLYGON [ 1 ] - 5 pts
Len: 16.709769332012375 Area: 6.511982813837381
B : MULTIPOLYGON [ 1 ] - 5 pts
Len: 16.70976933201237 Area: 6.51198281383738
The heuristic needs to be modified to be less stringent when evaluating very small differences in area. I'm not sure what a suitable fix is, but will think about it. Suggestions welcome.
In case of DIFFERENCE
operation this is the current code:
public static boolean isResultAreaConsistent(Geometry geom0, Geometry geom1, int opCode, Geometry result) {
// ...
double areaResult = result.getArea();
double areaA = geom0.getArea();
double areaB = geom1.getArea();
// ...
isConsistent = isLess(areaResult, areaA, AREA_HEURISTIC_TOLERANCE)
&& isGreater(areaResult, areaA - areaB, AREA_HEURISTIC_TOLERANCE);
AREA_HEURISTIC_TOLERANCE
is 0.1
and the comparisons are defined as
private static boolean isLess(double v1, double v2, double tol) {
return v1 <= v2 * (1 + tol);
}
private static boolean isGreater(double v1, double v2, double tol) {
return v1 >= v2 * (1 - tol);
}
If I understand correctly, the intention is to check that:
areaOfResult
) is smaller or equal to the 110% of the area of the first input geometry (as when we remove something from the input geometry, it cannot become bigger)
areaOfResult
) is >=
to 90% of the difference of areas between two input geometries.
isGreater(positiveAlmostZero, 0, tolerance)
, which means positiveAlmostZero>= 0 * 90%
, i.e. the check also works.isGreater(0, negativeAlmostZero, tolerance)
would result in return 0 >= negativeAlmostZero * 90%
, which is true
, so the check works.isGreater(0, positiveAlmostZero, tolerance)
would result in return 0 >= positiveAlmostZero * 90%
, which is false
(like in the reported case), so the check does not work.I tried to think of possible solutions, but did not come up with any definitive answer quickly (but I can think more of it if needed).
One line of thinking was to add special handling for 0 or almost-zero values. But I did not come up with a way how to come up with "what is almost zero" without hardcoding a constant, e.g. 0.000001
. Maybe that could be expressed as some % of the area of A or B (whichever is smaller and non-empty)? But that would likely require passing an extra arg to isGreater
.
Another idea was to base it on relative difference between sizes (one such example function is provided at the bottom of this page: https://c-faq.com/fp/fpequal.html), but then I think it does not work for non-near-zero cases as if there is no overlap between two similarly-sized polygons, the relative difference between area of the result (area A) and difference between areas (close to 0) can be very large.
See #1005 for a fix for this issue.
@grimsa it would be great if you can confirm the fix works.
@dr-jts #1005 seems to fix the issue we had - thank you for the fix! It will let us benefit from JTS 1.20.0 once it is released.
However, while testing I noticed an unrelated minor change in the output of negative buffer operation since 1.19.0 - reported it in https://github.com/locationtech/jts/issues/1007
After upgrading JTS from 1.18.2 to 1.19.0 ran into a number of tests that use difference operation failing with
TopologyException: Result area inconsistent with overlay operation
.I tracked it down to
OverlayUtil#isResultAreaConsistent
(which was added in https://github.com/locationtech/jts/pull/812):In our case we are performing a difference operation on two almost-exactly-the-same geometries and expect an empty result.
Note: we are using snapping noder like in OverlayNGSnappingFunctions code.
Test case 1
This case fails with
TopologyException
:In this case
OverlayUtil#isResultAreaConsistent
is reached once, and the expression inisGreater
check works out to be:v1 >= v2 * (1 - tol);
0 >= 8.8819E-16 * (1 - 0.1)
==>false
Test case 2
This case passes with the same data:
In this case it seems
OverlayNGRobust
is used instead, which results in multiple calls toOverlayUtil#isResultAreaConsistent
(with slightly different values) and eventually it succeeds in passing the heuristic check.Question
To me this feels like a bug in the heuristic check, but maybe our utility function for the difference operation is implemented in a suboptimal way?
If it's indeed a bug, and the fix is relatively simple (for someone who does not know JTS internals well), we'd be happy to contribute a PR with the fix.