locationtech / jts

The JTS Topology Suite is a Java library for creating and manipulating vector geometry.
Other
1.94k stars 441 forks source link

GeometryCollection#union with slightly overlapping input polygons excludes some polygons in its result #784

Open grimsa opened 2 years ago

grimsa commented 2 years ago

(Not sure if this is a bug or just me using JTS in an unsupported way)

The need: union ~5-20 polygons that may be (slightly) overlapping

My current implementation uses new GeometryCollection(polygons).union() and it has worked reliably so far.

However, I found one case where the union does not fail, yet the produced polygon is missing large areas.

Input: image

Union result (red) over input (blue): image

Versions used:

Test case:

    @Test
    void union_slightlyOverlappingPolygons_areasShouldNotBeLost() throws ParseException {
        var gf = new GeometryFactory();
        Polygon initialPolygon = (Polygon) new WKTReader().read(
                "POLYGON ((-17.055671609725188 0.0838585273938056, -17.219533690879903 8.580102931174729, -12.78759751645274 4.315883851167196, -12.707594043193543 0.1677170531469111, -17.055671609725188 0.0838585273938056))"
        );
        Function<double[][], Polygon> createPolygon = points -> gf.createPolygon(
                IntStream.range(0, points[0].length)
                        .mapToObj(index -> new CoordinateXY(points[0][index], points[1][index]))
                        .toArray(CoordinateXY[]::new)
        );
        var p1 = createPolygon.apply(new double[][]{
                {-16.15907384118054, -16.28767148661218, -13.719459554848422, -13.639456293556348, -16.15907384118054},
                {1.0157206673651495, 7.6835050482212575, 5.212481574525698, 1.0643148816523527, 1.0157206673651495}
        });
        var p2 = createPolygon.apply(new double[][]{
                {-12.725226307666448, -13.639456293556348, -13.719459768757051, -12.78759751645274, -12.725226307666448},
                {1.0819470369308437, 1.0643148816523527, 5.212481780339414, 4.315883851167196, 1.0819470369308437}
        });
        var p3 = createPolygon.apply(new double[][]{
                {-13.621824029083443, -16.14144162383529, -16.15907384118054, -13.639456293556348, -13.621824029083443},
                {0.15008489786842003, 0.10149068267229658, 1.0157206673651493, 1.0643148816523527, 0.15008489786842003}
        });
        var p4 = createPolygon.apply(new double[][]{
                {-17.219533690879903, -16.28767148661218, -16.15907384118054, -17.073303827070436, -17.219533690879903},
                {8.580102931174729, 7.683505048221257, 1.0157206673651495, 0.9980885120866584, 8.580102931174729}
        });
        var p5 = createPolygon.apply(new double[][]{
                {-12.707594043193543, -13.621824029083443, -13.639456293556348, -12.725226307666448, -12.707594043193543},
                {0.1677170531469111, 0.15008489786842005, 1.0643148816523527, 1.0819470369308437, 0.1677170531469111}
        });
        var p6 = createPolygon.apply(new double[][]{
                {-17.055671609725188, -17.073303827070436, -16.15907384118054, -16.14144162383529, -17.055671609725188},
                {0.0838585273938056, 0.9980885120866584, 1.0157206673651493, 0.1014906826722966, 0.0838585273938056}
        });
        Polygon[] polygons = {
                p1, p2, p3, p4, p5, p6
        };

        var gcUnion = gf.createGeometryCollection(polygons).union();
        var mpUnion = gf.createMultiPolygon(polygons).union();
        var indUnion = Arrays.stream(polygons)
                .map(Geometry.class::cast)
                .reduce(Geometry::union)
                .orElseThrow();

        assertAll(
                () -> assertEquals(initialPolygon.getArea(), gcUnion.getArea(), 0.0001), // Fail
                () -> assertEquals(initialPolygon.getArea(), mpUnion.getArea(), 0.0001), // Fail
                () -> assertEquals(initialPolygon.getArea(), indUnion.getArea(), 0.0001) // Pass
        );
    }

Some notes:

So I guess my questions are:

  1. Should GeometryCollection/MultiPolygon produce a correct union for this slightly overlapping set of polygons? (i.e. is it supported)
  2. If not, what would be the preferred way to union ~5-20 simple polygons - would it be doing it one by one (like in the test), or is there some better way?
dr-jts commented 2 years ago

Thanks for the detailed issue! I don't see anything wrong with the way you are using JTS (and it's interesting seeing JTS being used with modern functional Java code).

This does look like some sort of bug. I can run your test code and get the failure. The odd thing is that if I load the input GeometryCollection into the TestBuilder and run it it works correctly!

GEOMETRYCOLLECTION (POLYGON ((-16.15907384118054 1.0157206673651495, -16.28767148661218 7.6835050482212575, -13.719459554848422 5.212481574525698, -13.639456293556348 1.0643148816523527, -16.15907384118054 1.0157206673651495)), POLYGON ((-12.725226307666448 1.0819470369308437, -13.639456293556348 1.0643148816523527, -13.719459768757051 5.212481780339414, -12.78759751645274 4.315883851167196, -12.725226307666448 1.0819470369308437)), POLYGON ((-13.621824029083443 0.15008489786842, -16.14144162383529 0.1014906826722966, -16.15907384118054 1.0157206673651493, -13.639456293556348 1.0643148816523527, -13.621824029083443 0.15008489786842)), POLYGON ((-17.219533690879903 8.580102931174729, -16.28767148661218 7.683505048221257, -16.15907384118054 1.0157206673651495, -17.073303827070436 0.9980885120866584, -17.219533690879903 8.580102931174729)), POLYGON ((-12.707594043193543 0.1677170531469111, -13.621824029083443 0.1500848978684201, -13.639456293556348 1.0643148816523527, -12.725226307666448 1.0819470369308437, -12.707594043193543 0.1677170531469111)), POLYGON ((-17.055671609725188 0.0838585273938056, -17.073303827070436 0.9980885120866584, -16.15907384118054 1.0157206673651493, -16.14144162383529 0.1014906826722966, -17.055671609725188 0.0838585273938056)))

image

I'll keep digging and try and find out what's going on. It's possible it's something to do with numeric precision that only shows up when the data is loaded as Java double-precision literals - although would be surprising, given the extensive robustness heuristics in OverlayNG.

dr-jts commented 2 years ago

I think the problem is related to the fact that you are specifying 17 digits of precision in the input ordinates. For instance, this one. 0.15008489786842003. If I truncate this to 16 digits of precision (0.15008489786842) then the test case works.

I still think the various levels of robust geometric code in OverlayNG should handle this - but obviously it doesn't! Perhaps it's not surprising - 17 digits is right at the limit of double-precision floating-point representation.

I'll keep digging, but in the meantime you can round off your input numbers a bit.

dr-jts commented 2 years ago

The same problem occurs when computing the union of p3 and p5, so that simplifies testing.

dr-jts commented 2 years ago

A test case is here: https://github.com/dr-jts/jts/tree/fix-union-highprec

grimsa commented 2 years ago

Ran into a similar issue of union of polygons with very high precision coordinates producing an empty result. Works fine when serialized to WKT and loaded into test builder. Did not check against latest JTS snapshot.

Test Case:

    @Test
    void geometryCollectionUnion() {
        var gf = new GeometryFactory();
        var p1 = gf.createPolygon(new Coordinate[]{
                new Coordinate(-0.49524584515584785, 2.254321259525603),
                new Coordinate(-0.4518674095819571, 1.6850769474821623),
                new Coordinate(0.11737696012368323, 1.7284553797131785),
                new Coordinate(-3.208506829071115, -2.146129227235706),
                new Coordinate(-3.821129633016156, -1.6202633492159437),
                new Coordinate(-0.49524584515584785, 2.254321259525603)
        });

        var p2 = gf.createPolygon(new Coordinate[]{
                new Coordinate(-0.49524584515584785, 2.254321259525603, 6.986031855278883),
                new Coordinate(-0.4518674095819571, 1.6850769474821623, 6.986031855334046),
                new Coordinate(0.11737696012368318, 1.7284553797131785, 6.986031855278883),
                new Coordinate(-0.49524584515584785, 2.254321259525603, 6.986031855278883)
        });

        // Passes
        assertFalse(p2.union(p1).isEmpty(), "p2 union p1");
        // Passes
        assertFalse(p1.union(p2).isEmpty(), "p1 union p2");
        // Fails in JTS 1.18.2 with OverlayNG
        assertFalse(gf.createGeometryCollection(new Polygon[]{p1, p2}).union().isEmpty(), "geometry collection union");
    }