Repository: rasterio/rasterio

Issue:
Switch sense of rio-warp's --bounds from src to dst
For 1.0, let's harmonize rio-warp's `--dimensions`, `--res`, `--bounds`, and `--like` options so they all specify the output/destination raster. This will mean a breaking change for `--bounds`. I apologize for not catching the interface discrepancy earlier, everyone.

Plan: 
- raise a [FutureWarning](https://docs.python.org/2/library/exceptions.html#exceptions.FutureWarning) starting with the next release explaining the upcoming change to rio-warp's `--bounds` option.
- add a `--x-dst-bounds` option to impl the corrected feature.
- add a `--src-bounds` option to preserve the existing feature.
- prevent the GDAL faults (crashes and non-returning calls) that are possible when bad destination bounds are passed.

Sound good, @brendan-ward @perrygeo?



Discussion:
:+1:  with a question: How do we define "bad destination bounds"?

:+1

Would it simplify or complicate to preserve the existing options, but add a flag that indicates if they apply to the source, or the destination?

@perrygeo the only bounds checking that seemed obvious to me is against geographic coordinates (I think I did something in `rasterize` to do this), but then there are dateline / anti-meridian issues to work out.  This is much harder to do for most projections.

The problem is that the world bounding box in geographic isn't valid for projecting to web Mercator, which seems like a very common use case here.  GDAL seems  to segfault or return bad input when you go too far north or south...

@brendan-ward Another alternative would be the equivalent of gdalwarp's `-te_srs` which allows you to define the bounds in an arbitrary crs. 

But I like the `--bounds` and `--src-bounds` proposal for it's simplicity - it feels like a pretty intuitive interface to me. 

re: valid bounds, yes the validity with regards to the projection details are tough. I was thinking checking bounds just in terms of what would prevent obvious segfaults (like specify meters when projecting to 4326). Not sure what the best solution is here.

In one test (using `--vv -s`), I'm seeing

```
tests/test_rio_warp.py::test_warp_reproject_bounds_res
...
DEBUG:rasterio:Created transformer
DEBUG:rasterio:Setting warp option  NUM_THREADS: 2
DEBUG:rasterio:Created warp options
DEBUG:rasterio:src_nodata: 255.0
DEBUG:rasterio:dst_nodata: 255.0
DEBUG:rasterio:Set transformer options
DEBUG:rasterio:Destination shape: (2000000, 1000000)
DEBUG:rasterio:Chunk and warp window: 0, 0, 1000000, 2000000
...
```

Refusing to produce 2x10^12 pixel output files might be the first step... am looking into that now.

@perrygeo `--bounds` and `--src-bounds` is fine with me.  I personally think that using a bounding box that is not in either source or target projection feels out of place here, so I'm not a fan of using gdalwarp's `-te_srs` in this case.


Resolving patch:
diff --git a/rasterio/rio/warp.py b/rasterio/rio/warp.py
--- a/rasterio/rio/warp.py
+++ b/rasterio/rio/warp.py
@@ -22,27 +22,6 @@
 MAX_OUTPUT_HEIGHT = 100000
 
 
-def bounds_handler(ctx, param, value):
-    """Warn about future usage changes."""
-    if value:
-        click.echo(
-            "Future Warning: "
-            "the semantics of the `--bounds` option will change in Rasterio "
-            "version 1.0 from bounds of the source dataset to bounds of the "
-            "destination dataset.", err=True)
-    return value
-
-
-def x_dst_bounds_handler(ctx, param, value):
-    """Warn about future usage changes."""
-    if value:
-        click.echo(
-            "Future Warning: "
-            "the `--x-dst-bounds` option will be removed in Rasterio version "
-            "1.0 in favor of `--bounds`.", err=True)
-    return value
-
-
 @click.command(short_help='Warp a raster dataset.')
 @files_inout_arg
 @options.output_opt
@@ -59,18 +38,10 @@ def x_dst_bounds_handler(ctx, param, value):
     '--src-bounds',
     nargs=4, type=float, default=None,
     help="Determine output extent from source bounds: left bottom right top "
-         "(note: for future backwards compatibility in 1.0).")
+         ". Cannot be used with destination --bounds")
 @click.option(
-    '--x-dst-bounds',
-    nargs=4, type=float, default=None, callback=x_dst_bounds_handler,
-    help="Set output extent from bounding values: left bottom right top "
-         "(note: this option will be removed in 1.0).")
-@click.option(
-    '--bounds',
-    nargs=4, type=float, default=None, callback=bounds_handler,
-    help="Determine output extent from source bounds: left bottom right top "
-         "(note: the semantics of this option will change to those of "
-         "`--x-dst-bounds` in version 1.0).")
+    '--bounds', '--dst-bounds', nargs=4, type=float, default=None,
+    help="Determine output extent from destination bounds: left bottom right top")
 @options.resolution_opt
 @click.option('--resampling', type=click.Choice([r.name for r in Resampling]),
               default='nearest', help="Resampling method.",
@@ -87,7 +58,7 @@ def x_dst_bounds_handler(ctx, param, value):
 @options.creation_options
 @click.pass_context
 def warp(ctx, files, output, driver, like, dst_crs, dimensions, src_bounds,
-         x_dst_bounds, bounds, res, resampling, src_nodata, dst_nodata, threads, check_invert_proj,
+         dst_bounds, res, resampling, src_nodata, dst_nodata, threads, check_invert_proj,
          force_overwrite, creation_options):
     """
     Warp a raster dataset.
@@ -151,11 +122,9 @@ def warp(ctx, files, output, driver, like, dst_crs, dimensions, src_bounds,
             out_kwargs['driver'] = driver
 
             # Sort out the bounds options.
-            src_bounds = bounds or src_bounds
-            dst_bounds = x_dst_bounds
             if src_bounds and dst_bounds:
                 raise click.BadParameter(
-                    "Source and destination bounds may not be specified "
+                    "--src-bounds and destination --bounds may not be specified "
                     "simultaneously.")
 
             if like:


Test patch:
diff --git a/tests/test_rio_warp.py b/tests/test_rio_warp.py
--- a/tests/test_rio_warp.py
+++ b/tests/test_rio_warp.py
@@ -199,6 +199,19 @@ def test_warp_no_reproject_bounds_res(runner, tmpdir):
             assert output.height == 67
 
 
+    # dst-bounds should be an alias to bounds
+    outputname = str(tmpdir.join('test2.tif'))
+    out_bounds = [-11850000, 4810000, -11849000, 4812000]
+    result = runner.invoke(warp.warp,[srcname, outputname,
+                                      '--res', 30,
+                                      '--dst-bounds', ] + out_bounds)
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+    with rasterio.open(srcname) as src:
+        with rasterio.open(outputname) as output:
+            assert np.allclose(output.bounds, out_bounds)
+
+
 def test_warp_reproject_dst_crs(runner, tmpdir):
     srcname = 'tests/data/RGB.byte.tif'
     outputname = str(tmpdir.join('test.tif'))
@@ -276,13 +289,13 @@ def test_warp_reproject_bounds_no_res(runner, tmpdir):
 
 
 def test_warp_reproject_multi_bounds_fail(runner, tmpdir):
-    """Mixing --bounds and --x-dst-bounds fails."""
+    """Mixing --bounds and --src-bounds fails."""
     srcname = 'tests/data/shade.tif'
     outputname = str(tmpdir.join('test.tif'))
     out_bounds = [-11850000, 4810000, -11849000, 4812000]
     result = runner.invoke(warp.warp, [srcname, outputname,
                                        '--dst-crs', 'EPSG:4326',
-                                       '--x-dst-bounds'] + out_bounds +
+                                       '--src-bounds'] + out_bounds +
                                        ['--bounds'] + out_bounds)
     assert result.exit_code == 2
 
@@ -294,22 +307,11 @@ def test_warp_reproject_bounds_crossup_fail(runner, tmpdir):
     out_bounds = [-11850000, 4810000, -11849000, 4812000]
     result = runner.invoke(warp.warp, [srcname, outputname,
                                        '--dst-crs', 'EPSG:4326',
-                                       '--res', 0.001, '--x-dst-bounds', ]
+                                       '--res', 0.001, '--bounds', ]
                                        + out_bounds)
     assert result.exit_code == 2
 
 
-def test_warp_reproject_bounds_res_future_warning(runner, tmpdir):
-    """Use of --bounds results in a warning from the 1.0 future."""
-    srcname = 'tests/data/shade.tif'
-    outputname = str(tmpdir.join('test.tif'))
-    out_bounds = [-11850000, 4810000, -11849000, 4812000]
-    result = runner.invoke(
-                warp.warp, [srcname, outputname, '--dst-crs', 'EPSG:4326',
-                            '--res', 0.001, '--bounds'] + out_bounds)
-    assert "Future Warning" in result.output
-
-
 def test_warp_reproject_src_bounds_res(runner, tmpdir):
     """--src-bounds option works."""
     srcname = 'tests/data/shade.tif'
@@ -333,13 +335,13 @@ def test_warp_reproject_src_bounds_res(runner, tmpdir):
 
 
 def test_warp_reproject_dst_bounds(runner, tmpdir):
-    """--x-dst-bounds option works."""
+    """--bounds option works."""
     srcname = 'tests/data/shade.tif'
     outputname = str(tmpdir.join('test.tif'))
     out_bounds = [-106.45036, 39.6138, -106.44136, 39.6278]
     result = runner.invoke(
         warp.warp, [srcname, outputname, '--dst-crs', 'EPSG:4326',
-                    '--res', 0.001, '--x-dst-bounds'] + out_bounds)
+                    '--res', 0.001, '--bounds'] + out_bounds)
     assert result.exit_code == 0
     assert os.path.exists(outputname)