Repository: rasterio/rasterio

Issue:
Reproduce gdalwarp's CRS-limited output extents
PR #574 wraps the GDAL algorithm that gdalwarp uses for suggested warp output. To reproduce gdalwarp's output extents in the case that the suggested output bumps up against coordinate system limits, we must duplicate the logic at https://trac.osgeo.org/gdal/browser/branches/1.11/gdal/apps/gdalwarp.cpp#L1723 as pointed out by @brendan-ward in #555.

/cc @perrygeo



Discussion:
So in an effort to write a test case for this, I'm starting to suspect that `GDALSuggestedWarpOutput` already does this work for us. Or, put another way, I can't come up with a way to make  `calculate_default_transform` give us a suggested output that exeeds the coordinate system limits

```
 def test_crs_limits():

    # round trip a transform from out-of-bounds coords and 
    # see if it clips to crs limits already

    with rasterio.drivers():
        at, aw, ah = calculate_default_transform(
            src_crs={'init': 'EPSG:4326'},
            dst_crs={'init': 'EPSG:3857'},
            width=380,
            height=380,
            left=-190,
            bottom=-100,
            right=190,
            top=100)

    with rasterio.drivers():
        bt, bw, bh = calculate_default_transform(
            src_crs={'init': 'EPSG:3857'},
            dst_crs={'init': 'EPSG:4326'},
            width=aw,
            height=ah,
            left=at.c,
            bottom=at.f + (ah * at.e),
            right=at.c + (aw * at.a),
            top=at.f)

    assert bt.f >= -180.0
    assert bt.c <= 90.0
    assert bt.c + (bw * bt.a) <= 180.0
    assert bt.f + (bh * bt.e) >= -90.0
```

So there's clearly some sort of coordinate system limit clipping going on already. 

Next steps, research why gdalwarp implemented this heuristic and try to understand what edge case it was trying to solve that suggested warp output doesn't already handle

So here's what I don't get about that chunk of code in gdalwarp: The actual work is done in `GDALSuggestedWarpOutput2` and the whole thing is conditional on [`CHECK_WITH_INVERT_PROJ`](https://trac.osgeo.org/gdal/wiki/ConfigOptions#CHECK_WITH_INVERT_PROJ) 
- If NO, warp outputs are not checked
- If YES, warp outputs are checked and adjusted
- If NULL (the only case that would trigger that logic) it loops through to determine if invert checking is even necessary and if it is, resets to YES and _reruns_ GDALSuggestedWarpOutput2. So it **effectively defaults to YES** anyways.

I'm having trouble getting my head around the logic here. Seems like running the suggested warp output algorithm twice would be wasteful; if you want that to be the default, just run it once with YES, right? Unless there is a huge penalty in suggested warp runtimes? EDIT: there isn't

Since rasterio users can specify the configuration option whichever way they'd prefer

```
with rasterio.drivers(CHECK_WITH_INVERT_PROJ=True):
    ....
```

I'm not seeing the need to include the logic in `rio warp` as it basically just serves to automatically make the decision for the undecided. If you want the check, just say so!  We could have `calculate_default_transform` take a boolean keyword argument, `check_invert=True|False`

https://trac.osgeo.org/gdal/ticket/2305 has some good use cases where this proj inversion checking might make a difference

Here's a quick investigation into the effects of `CHECK_WITH_INVERT_PROJ`.

Source image in epsg:4326
![world tif](https://cloud.githubusercontent.com/assets/1151287/13937458/6059606e-ef9b-11e5-9ec5-0ae7bb681704.jpg)

Warp to 3758, a transverse mercator projection focused on Wyoming, **with** the invert proj check

```
$ export CHECK_WITH_INVERT_PROJ=YES
$ rio warp --force-overwrite --dst-crs epsg:3758 tests/data/warp_test.tif  ~/Desktop/check.tif
```

![check tif](https://cloud.githubusercontent.com/assets/1151287/13937472/795740c2-ef9b-11e5-8f57-e69e2faa7f54.jpg)

And **without** the invert proj check

```
$ export CHECK_WITH_INVERT_PROJ=NO
$ rio warp --force-overwrite --dst-crs epsg:3758 tests/data/warp_test.tif  ~/Desktop/nocheck.tif
ERROR:GDAL:CPLE_AppDefined in latitude or longitude exceeded limits
....
ERROR:GDAL:CPLE_AppDefined in latitude or longitude exceeded limits
ERROR:GDAL:CPLE_AppDefined in Reprojection failed, err = -14, further errors will be suppressed on the transform object.
```

![nocheck tif](https://cloud.githubusercontent.com/assets/1151287/13937505/b65e3d0e-ef9b-11e5-9959-a0142789a0e5.jpg)

Confirmed that behavior can be controlled using kwargs to the drivers context manager

```
with rasterio.drivers(CHECK_WITH_INVERT_PROJ=True):
    ....
```

I think we should expose this as a `calculate_default_transform(..., check_invert_proj=True)` argument and as a `rio warp --check-invert-proj` option. No need to have complex logic driven by environment variables, let's just set default to True (which it effectively is anyways in gdalwarp) and let the user decide.

And `%timeit` showed no speed difference with/without `CHECK_WITH_INVERT_PROJ` so there's no performance hit either way.

Down the rabbit hole we go...

Some investigations into controlling the behavior with environment variables proved... interesting.

Here is the result of setting the `rasterio.driver` env at odds with the shell environment variables.

`CHECK_WITH_INVERT_PROJ=YES rio warp --check-invert-proj no`
![yes-env_no-opt](https://cloud.githubusercontent.com/assets/1151287/13938310/0a56f17e-efa3-11e5-8918-6d436efed0b7.jpg)

`CHECK_WITH_INVERT_PROJ=NO rio warp --check-invert-proj yes`
![no-env_yes-opt_threads-many](https://cloud.githubusercontent.com/assets/1151287/13938800/8c55164a-efa5-11e5-83ef-71780c94db93.jpg)

The bounds get calculated with NO but _some_ of the write threads say YES. In other words the shell environment variable is overriding the environment set up by `rasterio.drivers` but only in a few threads.

If we set `--threads 1`, both the bounds calculation and the warping itself behave as expected, i.e. it obeys the `rasterio.drivers` environment

`CHECK_WITH_INVERT_PROJ=YES rio warp --check-invert-proj no --threads 1`
![yes-env_no-opt_threads-1](https://cloud.githubusercontent.com/assets/1151287/13938387/ccd4d2a2-efa3-11e5-96d1-f0aac7ab0fcb.jpg)

`CHECK_WITH_INVERT_PROJ=NO rio warp --check-invert-proj yes --threads 1`
![no-env_yes-opt_threads-1](https://cloud.githubusercontent.com/assets/1151287/13938388/ccd512e4-efa3-11e5-9e5c-9809211f4372.jpg)


Resolving patch:
diff --git a/rasterio/rio/warp.py b/rasterio/rio/warp.py
--- a/rasterio/rio/warp.py
+++ b/rasterio/rio/warp.py
@@ -68,14 +68,16 @@ def x_dst_bounds_handler(ctx, param, value):
 @click.option('--resampling', type=click.Choice([r.name for r in Resampling]),
               default='nearest', help="Resampling method.",
               show_default=True)
-@click.option('--threads', type=int, default=2,
+@click.option('--threads', type=int, default=1,
               help='Number of processing threads.')
+@click.option('--check-invert-proj', type=bool, default=True,
+              help='Constrain output to valid coordinate region in dst-crs')
 @options.force_overwrite_opt
 @options.creation_options
 @click.pass_context
 def warp(ctx, files, output, driver, like, dst_crs, dimensions, src_bounds,
-         x_dst_bounds, bounds, res, resampling, threads, force_overwrite,
-         creation_options):
+         x_dst_bounds, bounds, res, resampling, threads, check_invert_proj,
+         force_overwrite, creation_options):
     """
     Warp a raster dataset.
 
@@ -132,7 +134,8 @@ def warp(ctx, files, output, driver, like, dst_crs, dimensions, src_bounds,
         # Expand one value to two if needed
         res = (res[0], res[0]) if len(res) == 1 else res
 
-    with rasterio.drivers(CPL_DEBUG=verbosity > 2):
+    with rasterio.drivers(CPL_DEBUG=verbosity > 2,
+                          CHECK_WITH_INVERT_PROJ=check_invert_proj):
         with rasterio.open(files[0]) as src:
             l, b, r, t = src.bounds
             out_kwargs = src.meta.copy()
diff --git a/rasterio/warp.py b/rasterio/warp.py
--- a/rasterio/warp.py
+++ b/rasterio/warp.py
@@ -283,12 +283,21 @@ def calculate_default_transform(
     Returns
     -------
     tuple of destination affine transform, width, and height
+
+    Note
+    ----
+    Must be called within a raster.drivers() context
+
+    Some behavior of this function is determined by the
+    CHECK_WITH_INVERT_PROJ environment variable
+        YES: constrain output raster to extents that can be inverted
+             avoids visual artifacts and coordinate discontinuties.
+        NO:  reproject coordinates beyond valid bound limits
     """
-    with rasterio.drivers():
-        dst_affine, dst_width, dst_height = _calculate_default_transform(
-            src_crs, dst_crs,
-            width, height,
-            left, bottom, right, top)
+    dst_affine, dst_width, dst_height = _calculate_default_transform(
+        src_crs, dst_crs,
+        width, height,
+        left, bottom, right, top)
 
     # If resolution is specified, Keep upper-left anchored
     # adjust the transform resolutions


Test patch: