Repository: rasterio/rasterio

Issue:
rasterize produces holes for lists of GeometryCollection
<!--

WELCOME ABOARD!

Hi and welcome to the Rasterio project. We appreciate bug reports, questions
about documentation, and suggestions for new features. This issue template
isn't intended to ward you off; only to intercept and redirect one particular
category of reports, and to collect a few important facts that issue reporters
often omit.

You think you've found something? We believe you.

Please note: Rasterio contains extension modules and is thus susceptible to
C library compatibility issues. If you are reporting an installation or module
import issue, please note that this project only accepts reports about problems
with packages downloaded from the Python Package Index. Conda users should take
issues to one of the following trackers:

- https://github.com/ContinuumIO/anaconda-issues/issues
- https://github.com/conda-forge/rasterio-feedstock

-->

## Expected behavior and actual behavior.

I expected `rasterize(GeometryCollection([shp1, shp2]))` to produce the same result than `rasterize([GeometryCollection([shp1, shp2])])`. Instead, the latter produces a hole in the overlap, regardless of the direction of the exterior of the two polygons.

## Steps to reproduce the problem.

```
shp1 = Polygon.from_bounds(0, 0, 5, 5)
shp2 = Polygon.from_bounds(2, 2, 7, 7)

gc = GeometryCollection([shp1, shp2])
```

| Code   | rasterize([shp1, shp2], (10, 10))                                                                               | rasterize(gc, (10, 10))                                                                                         | rasterize([gc], (10, 10))                                                                                       |
|--------|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
| Result | ![Result 1](https://user-images.githubusercontent.com/316517/35390348-61ee19ac-01db-11e8-8ddf-29bb5dc46b5b.png) | ![Result 1](https://user-images.githubusercontent.com/316517/35390348-61ee19ac-01db-11e8-8ddf-29bb5dc46b5b.png) | ![Result 2](https://user-images.githubusercontent.com/316517/35390359-6bc328d2-01db-11e8-81b1-a4347ff4d475.png) |

## Operating system

Linux Mint 18.3

## Rasterio version and provenance

* libgdal 1.11.2 installed with conda
* geos 3.6.2 installed with conda from conda-forge
* shapely 1.6.4.post1 installed with pip 9.0.1
* rasterio 1.0a12 manylinux1 wheel installed from PyPI using pip 9.0.1.



Discussion:
Okay, `rasterize([shp1, shp2])` and `rasterize(gc)` are the same because `rasterize` takes an interator over geometries and a shapely `GeometryCollection` is an iterator over its parts: `list(gc) == [shp1, shp2]`. I, too, would expect `rasterize([gc])` to give the same results.

I think the only way this is not an upstream issue (in GDAL) is if there is a bug in Rasterio's OGR geometry factory specific to geometry collections. I'll take a look, but probably not until after 1.0, because we can document a work around for this in the short term.

@Juanlu001 would you be willing to see if updating GDAL to 2.2 fixes this?

Lastly, I'd like to point out that mixing conda packages, conda-forge packages, and the binary wheels I publish to PyPI is a perilous thing to do. My binary wheels are not compatible with conda. In general, I think C extension modules on PyPI can't be installed into a conda environment using pip.
I get the same behavior with GDAL 2.2. Just to make sure it's not a result of mixing binaries as you say, I installed rasterio from source to compile it against my libgdal:

```
$ conda install libgdal -c conda-forge
$ pip install --pre rasterio --no-binary :all:
```

And nothing changes.

> Lastly, I'd like to point out that mixing conda packages, conda-forge packages, and the binary wheels I publish to PyPI is a perilous thing to do.

I know... "don't do this at home" :)

> My binary wheels are not compatible with conda. In general, I think C extension modules on PyPI can't be installed into a conda environment using pip.

I do it all the time in Linux, and so far I have not found problems. Would you please point me to some past issue that was due to this?


Resolving patch:
diff --git a/rasterio/features.py b/rasterio/features.py
--- a/rasterio/features.py
+++ b/rasterio/features.py
@@ -259,7 +259,18 @@ def format_cast_error(param, dtype):
                 'Invalid geometry object at index {0}'.format(index)
             )
 
-        valid_shapes.append((geom, value))
+        if geom['type'] == 'GeometryCollection':
+            # GeometryCollections need to be handled as individual parts to
+            # avoid holes in output:
+            # https://github.com/mapbox/rasterio/issues/1253.
+            # Only 1-level deep since GeoJSON spec discourages nested
+            # GeometryCollections
+            for part in geom['geometries']:
+                valid_shapes.append((part, value))
+
+        else:
+            valid_shapes.append((geom, value))
+
         shape_values.append(value)
 
     if not valid_shapes:


Test patch:
diff --git a/tests/test_features.py b/tests/test_features.py
--- a/tests/test_features.py
+++ b/tests/test_features.py
@@ -420,6 +420,29 @@ def test_rasterize_geo_interface(geojson_polygon, basic_image_2x2):
     )
 
 
+def test_rasterize_geomcollection_no_hole():
+    """
+    Make sure that bug reported in
+    https://github.com/mapbox/rasterio/issues/1253
+    does not recur.  GeometryCollections are flattened to individual parts,
+    and should result in no holes where parts overlap.
+    """
+
+    geomcollection = {'type': 'GeometryCollection', 'geometries': [
+        {'type': 'Polygon',
+        'coordinates': (((0, 0), (0, 5), (5, 5), (5, 0), (0, 0)),)},
+        {'type': 'Polygon',
+        'coordinates': (((2, 2), (2, 7), (7, 7), (7, 2), (2, 2)),)}
+    ]}
+
+    expected = rasterize(geomcollection['geometries'], out_shape=DEFAULT_SHAPE)
+
+    assert np.array_equal(
+        rasterize([geomcollection], out_shape=DEFAULT_SHAPE),
+        expected
+    )
+
+
 def test_rasterize_invalid_geom():
     """Invalid GeoJSON should fail with exception"""