Repository: rasterio/rasterio

Issue:
Should rio clip fill nodata at borders?
Hello lovely people :hugs:,

I'm using `rio clip` to quickly cut out z10 tiles for raster image. The raster image is not divisible by the z10 tiles, so I expect the border tiles to contain nodata. But more importantly I expect all z10 tiles I cut out to be of the same dimension. This is not the case with rio clip right now.

Here is an example where I first convert my raster image to WGS84 and then cut it into z10 tiles where the file name is the z10 quad key id.

```
rio warp in.tif wgs84.tif --dst-crs EPSG:4326
rio bounds wgs84.tif | mercantile tiles 10 > z10.txt
 
while read tile; do
  rio clip wgs84.tif $(mercantile quadkey "$tile").tif --bounds "$(mercantile shapes --extents "$tile")"
done < z10.txt
```

This results in
- many raster images of pixel size 2857x1744
- some raster images (at the borders) with smaller dimensions; the bounds are not filled with nodata

Should rio clip use boundless windows and fill with nodata, to respect the user's bounds?

(rio 1.1.3, tested in ubuntu 18.04 docker container with python 3.6)


Discussion:
:wave: @daniel-j-h. Currently, rio-clip does not pad. It trims and the trimming happens here: https://github.com/mapbox/rasterio/blob/master/rasterio/rio/clip.py#L111-L112. We could make that statement optional, defaulting to True, if that helps you?

Otherwise, do you find the tile sizes consistent and correct? At first I thought 2857x1744 looked wrong, but then I realized you're extracting mercator zoom 10 tiles in long/lat space, yes?
Making the trimming optional and filling with nodata would be awesome! :rocket: 

Thanks for the take on the projection - you are right it should be `3857` - I'm trying to extract mercator zoom 10 tiles for a raster image in a different projection. Changing it to

```
rio warp in.tif 3857.tif --dst-crs EPSG:3857
rio bounds 3857.tif | mercantile tiles 10 > z10.txt
 
while read tile; do
  rio clip 3857.tif $(mercantile quadkey "$tile").tif --bounds "$(mercantile shapes --extents --mercator "$tile")"
done < z10.txt
```

(changing the crs in rio warp and adding mercator flag to mercantile shapes)

results in 2370x2370px raster tiles now (except for at the borders).

Resolving patch:
diff --git a/rasterio/rio/clip.py b/rasterio/rio/clip.py
--- a/rasterio/rio/clip.py
+++ b/rasterio/rio/clip.py
@@ -50,9 +50,24 @@
 @projection_projected_opt
 @options.overwrite_opt
 @options.creation_options
+@click.option(
+    "--with-complement/--without-complement",
+    default=False,
+    help="Include the relative complement of the raster in the given bounds (giving a larger result), else return results only from the intersection of the raster and the bounds (the default).",
+)
 @click.pass_context
-def clip(ctx, files, output, bounds, like, driver, projection,
-         overwrite, creation_options):
+def clip(
+    ctx,
+    files,
+    output,
+    bounds,
+    like,
+    driver,
+    projection,
+    overwrite,
+    creation_options,
+    with_complement,
+):
     """Clips a raster using projected or geographic bounds.
 
     \b
@@ -108,8 +123,11 @@ def clip(ctx, files, output, bounds, like, driver, projection,
                 raise click.UsageError('--bounds or --like required')
 
             bounds_window = src.window(*bounds)
-            bounds_window = bounds_window.intersection(
-                Window(0, 0, src.width, src.height))
+
+            if not with_complement:
+                bounds_window = bounds_window.intersection(
+                    Window(0, 0, src.width, src.height)
+                )
 
             # Get the window with integer height
             # and width that contains the bounds window.
@@ -133,6 +151,11 @@ def clip(ctx, files, output, bounds, like, driver, projection,
                 del out_kwargs['blockysize']
                 logger.warning("Blockysize removed from creation options to accomodate small output height")
 
-            with rasterio.open(output, 'w', **out_kwargs) as out:
-                out.write(src.read(window=out_window,
-                                   out_shape=(src.count, height, width)))
+            with rasterio.open(output, "w", **out_kwargs) as out:
+                out.write(
+                    src.read(
+                        window=out_window,
+                        out_shape=(src.count, height, width),
+                        boundless=True,
+                    )
+                )


Test patch:
diff --git a/tests/test_rio_convert.py b/tests/test_rio_convert.py
--- a/tests/test_rio_convert.py
+++ b/tests/test_rio_convert.py
@@ -1,15 +1,13 @@
-import sys
 import os
-import logging
-import numpy as np
+
 from click.testing import CliRunner
+import numpy as np
+import pytest
 
 import rasterio
 from rasterio.rio.main import main_group
 
 
-logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
-
 TEST_BBOX = [-11850000, 4804000, -11840000, 4808000]
 
 
@@ -17,11 +15,12 @@ def bbox(*args):
     return ' '.join([str(x) for x in args])
 
 
-def test_clip_bounds(runner, tmpdir):
+@pytest.mark.parametrize("bounds", [bbox(*TEST_BBOX)])
+def test_clip_bounds(runner, tmpdir, bounds):
     output = str(tmpdir.join('test.tif'))
     result = runner.invoke(
-        main_group,
-        ['clip', 'tests/data/shade.tif', output, '--bounds', bbox(*TEST_BBOX)])
+        main_group, ["clip", "tests/data/shade.tif", output, "--bounds", bounds]
+    )
     assert result.exit_code == 0
     assert os.path.exists(output)
 
@@ -29,6 +28,29 @@ def test_clip_bounds(runner, tmpdir):
         assert out.shape == (419, 173)
 
 
+@pytest.mark.parametrize("bounds", [bbox(*TEST_BBOX)])
+def test_clip_bounds_with_complement(runner, tmpdir, bounds):
+    output = str(tmpdir.join("test.tif"))
+    result = runner.invoke(
+        main_group,
+        [
+            "clip",
+            "tests/data/shade.tif",
+            output,
+            "--bounds",
+            bounds,
+            "--with-complement",
+        ],
+    )
+    assert result.exit_code == 0
+    assert os.path.exists(output)
+
+    with rasterio.open(output) as out:
+        assert out.shape == (419, 1047)
+        data = out.read()
+        assert (data[420:, :] == 255).all()
+
+
 def test_clip_bounds_geographic(runner, tmpdir):
     output = str(tmpdir.join('test.tif'))
     result = runner.invoke(
@@ -106,9 +128,16 @@ def test_clip_overwrite_with_option(runner, tmpdir):
     assert result.exit_code == 0
 
     result = runner.invoke(
-        main_group, [
-        'clip', 'tests/data/shade.tif', output, '--bounds', bbox(*TEST_BBOX),
-        '--overwrite'])
+        main_group,
+        [
+            "clip",
+            "tests/data/shade.tif",
+            output,
+            "--bounds",
+            bbox(*TEST_BBOX),
+            "--overwrite",
+        ],
+    )
     assert result.exit_code == 0