Repository: rasterio/rasterio

Issue:
rio merge assumes nodata
Running `rio merge` over a set of images without a nodata value results in an error:

```
(rio-test)$ rio merge warped.tif merged.tif
ERROR:rio:Failed. Exception caught
Traceback (most recent call last):
  File "/Users/amit/Mapbox/rasterio/rasterio/rio/merge.py", line 50, in merge
    dest.fill(nodataval)
TypeError: long() argument must be a string or a number, not 'NoneType'
```

Checking for nodata in `rasterio/rio/merge.py` still results in a lower level error (https://github.com/mapbox/rasterio/commit/6b251f8261fd1b3e785dc73462212741b6bd62b7):

```
(rio-test)$ rio merge warped.tif merged.tif
ERROR:rio:Failed. Exception caught
Traceback (most recent call last):
  File "/Users/amit/Mapbox/rasterio/rasterio/rio/merge.py", line 55, in merge
    data = src.read()
  File "rasterio/_io.pyx", line 654, in rasterio._io.RasterReader.read (rasterio/_io.c:8075)
    arr.fill(ndv)
TypeError: long() argument must be a string or a number, not 'NoneType'
```

Patching this case throws the error down further until a regular numpy array is confused for a masked array (https://github.com/mapbox/rasterio/commit/c271c4ee23531db7c24208c85f56d04d79acc851):

```
(rio-test)$ rio merge warped.tif merged.tif
/Users/amit/Mapbox/rasterio/rasterio/rio/merge.py:58: FutureWarning: comparison to `None` will result in an elementwise object comparison in the future.
  dest==nodataval, data.mask==False))
ERROR:rio:Failed. Exception caught
Traceback (most recent call last):
  File "/Users/amit/Mapbox/rasterio/rasterio/rio/merge.py", line 58, in merge
    dest==nodataval, data.mask==False))
AttributeError: 'numpy.ndarray' object has no attribute 'mask'
```

/cc @sgillies 



Discussion:
I'm disappointed that c271c4e isn't the fix because I just did the same thing yesterday in https://github.com/mapbox/rasterio/commit/290184b4be5febb365d043f8d71b1095955de5b1. 

Funny we pushed the same change. How about some type of check that determines if the data is a regular ndarray or masked array? https://github.com/mapbox/rasterio/commit/d6114a7cbd0087515dd254b35f4b994c76333b00

btw, that commit is still broken - just an idea.


Resolving patch:
diff --git a/rasterio/rio/merge.py b/rasterio/rio/merge.py
--- a/rasterio/rio/merge.py
+++ b/rasterio/rio/merge.py
@@ -36,31 +36,39 @@ def merge(ctx, files, driver):
             with rasterio.open(files[0]) as first:
                 kwargs = first.meta
                 kwargs['transform'] = kwargs.pop('affine')
-                dest = np.empty((first.count,) + first.shape, 
+                dest = np.zeros((first.count,) + first.shape, 
                     dtype=first.dtypes[0])
 
+            nodataval = next(dest.flat)
             if os.path.exists(output):
                 dst = rasterio.open(output, 'r+')
-                nodataval = dst.nodatavals[0]
+                nodataval = dst.nodatavals[0] or nodataval
             else:
                 kwargs['driver'] == driver
                 dst = rasterio.open(output, 'w', **kwargs)
-                nodataval = first.nodatavals[0]
+                nodataval = first.nodatavals[0] or nodataval
 
-            dest.fill(nodataval)
+            if nodataval:
+                dest.fill(nodataval)
 
             for fname in reversed(files):
                 with rasterio.open(fname) as src:
                     data = src.read()
-                    np.copyto(dest, data,
-                        where=np.logical_and(
-                        dest==nodataval, data.mask==False))
+                    try:
+                        where = np.logical_and(
+                                dest==nodataval, data.mask==False)
+                    except AttributeError:
+                        where = dest==nodataval
+                    np.copyto(dest, data, where=where)
 
             if dst.mode == 'r+':
                 data = dst.read()
-                np.copyto(dest, data,
-                    where=np.logical_and(
-                    dest==nodataval, data.mask==False))
+                try:
+                    where = np.logical_and(
+                            dest==nodataval, data.mask==False)
+                except AttributeError:
+                    where = dest==nodataval
+                np.copyto(dest, data, where=where)
 
             dst.write(dest)
             dst.close()


Test patch:
diff --git a/tests/test_rio_merge.py b/tests/test_rio_merge.py
--- a/tests/test_rio_merge.py
+++ b/tests/test_rio_merge.py
@@ -15,7 +15,7 @@
 
 # Fixture to create test datasets within temporary directory
 @fixture(scope='function')
-def test_data_dir(tmpdir):
+def test_data_dir_1(tmpdir):
     kwargs = {
         "crs": {'init': 'epsg:4326'},
         "transform": (-114, 0.2, 0, 46, 0, -0.1),
@@ -24,16 +24,45 @@ def test_data_dir(tmpdir):
         "driver": "GTiff",
         "width": 10,
         "height": 10,
-        "nodata": 0
+        "nodata": 1
     }
 
     with rasterio.drivers():
-        with rasterio.open(str(tmpdir.join('one.tif')), 'w', **kwargs) as dst:
+
+        with rasterio.open(str(tmpdir.join('a.tif')), 'w', **kwargs) as dst:
+            data = numpy.ones((10, 10), dtype=rasterio.uint8)
+            data[0:6, 0:6] = 255
+            dst.write_band(1, data)
+
+        with rasterio.open(str(tmpdir.join('b.tif')), 'w', **kwargs) as dst:
+            data = numpy.ones((10, 10), dtype=rasterio.uint8)
+            data[4:8, 4:8] = 254
+            dst.write_band(1, data)
+
+    return tmpdir
+
+
+@fixture(scope='function')
+def test_data_dir_2(tmpdir):
+    kwargs = {
+        "crs": {'init': 'epsg:4326'},
+        "transform": (-114, 0.2, 0, 46, 0, -0.1),
+        "count": 1,
+        "dtype": rasterio.uint8,
+        "driver": "GTiff",
+        "width": 10,
+        "height": 10
+        # these files have undefined nodata.
+    }
+
+    with rasterio.drivers():
+
+        with rasterio.open(str(tmpdir.join('a.tif')), 'w', **kwargs) as dst:
             data = numpy.zeros((10, 10), dtype=rasterio.uint8)
             data[0:6, 0:6] = 255
             dst.write_band(1, data)
 
-        with rasterio.open(str(tmpdir.join('two.tif')), 'w', **kwargs) as dst:
+        with rasterio.open(str(tmpdir.join('b.tif')), 'w', **kwargs) as dst:
             data = numpy.zeros((10, 10), dtype=rasterio.uint8)
             data[4:8, 4:8] = 254
             dst.write_band(1, data)
@@ -41,9 +70,26 @@ def test_data_dir(tmpdir):
     return tmpdir
 
 
-def test_merge(test_data_dir):
-    outputname = str(test_data_dir.join('merged.tif'))
-    inputs = [str(x) for x in test_data_dir.listdir()]
+def test_merge_with_nodata(test_data_dir_1):
+    outputname = str(test_data_dir_1.join('merged.tif'))
+    inputs = [str(x) for x in test_data_dir_1.listdir()]
+    inputs.sort()
+    runner = CliRunner()
+    result = runner.invoke(merge, inputs + [outputname])
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+    with rasterio.open(outputname) as out:
+        assert out.count == 1
+        data = out.read_band(1, masked=False)
+        expected = numpy.ones((10, 10), dtype=rasterio.uint8)
+        expected[0:6, 0:6] = 255
+        expected[4:8, 4:8] = 254
+        assert numpy.all(data == expected)
+
+
+def test_merge_without_nodata(test_data_dir_2):
+    outputname = str(test_data_dir_2.join('merged.tif'))
+    inputs = [str(x) for x in test_data_dir_2.listdir()]
     inputs.sort()
     runner = CliRunner()
     result = runner.invoke(merge, inputs + [outputname])
@@ -73,6 +119,15 @@ def test_merge_output_exists(tmpdir):
         assert out.count == 3
 
 
+def test_merge_output_exists_without_nodata(test_data_dir_2):
+    runner = CliRunner()
+    result = runner.invoke(
+        merge,
+        [str(test_data_dir_2.join('a.tif')),
+            str(test_data_dir_2.join('b.tif'))])
+    assert result.exit_code == 0
+
+
 def test_merge_err():
     runner = CliRunner()
     result = runner.invoke(