Repository: rasterio/rasterio

Issue:
Should check-invert-proj be the default?
- gdalwarp (through it's bounds check heuristic) effectively defaults to `check-invert-proj=True`
- rio warp explicitly defaults to `check-invert-proj=True` with the click option default.
- in the python API, the behavior depends on how you set up the environment. It defaults to `check-invert-proj=False` if not specified.

If python users want that behavior they must

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

But should we make this the default? And by what mechanism?



Discussion:
@perrygeo I just grepped GDAL's source for this config option.

``` bash
$ grep -R CHECK_WITH_INVERT_PROJ *
alg/gdaltransformer.cpp:        /* In case CHECK_WITH_INVERT_PROJ has been modified */
apps/gdalwarp.cpp:            if ( CPLGetConfigOption( "CHECK_WITH_INVERT_PROJ", NULL ) == NULL )
apps/gdalwarp.cpp:                /* If not, retry with CHECK_WITH_INVERT_PROJ=TRUE that forces ogrct.cpp */
apps/gdalwarp.cpp:                    CPLSetConfigOption( "CHECK_WITH_INVERT_PROJ", "TRUE" );
apps/gdalwarp.cpp:                    CPLDebug("WARP", "Recompute out extent with CHECK_WITH_INVERT_PROJ=TRUE");
ogr/ogrct.cpp:    bCheckWithInvertProj = CSLTestBoolean(CPLGetConfigOption( "CHECK_WITH_INVERT_PROJ", "NO" ));
```

Since this is only tested in 2 places in GDAL/OGR and has very limited application what would you think about adding a `check_invert_proj=True` keyword argument to `calculate_default_transform()`? Inside `_calculate_default_transform()` we would set/unset that config option around the call to `_gdal.GDALSuggestedWarpOutput2()`.

My only hesitation with that approach is that we'd effectively be overriding the GDALEnv with a keyword arg. That's not a bad thing, I just didn't see any precedent for it. It might lead to unexpected behavior if the user didn't read the docs closely:

```
with rasterio.drivers(CHECK_WITH_INVERT_PROJ=False):
    a = calculate_default_transform(*args)  # defaults to check_invert_proj=True
```

It'd mostly be a matter of docs, explaining which GDALEnv variables are overridden by python function args.


Resolving patch:
diff --git a/rasterio/env.py b/rasterio/env.py
--- a/rasterio/env.py
+++ b/rasterio/env.py
@@ -16,6 +16,10 @@
 
 log = logging.getLogger(__name__)
 
+# Rasterio defaults
+default_options = {
+    'CHECK_WITH_INVERT_PROJ': True
+}
 
 class Env(object):
     """Abstraction for GDAL and AWS configuration
@@ -154,6 +158,7 @@ def defenv():
         log.debug("Environment %r exists", _env)
     else:
         _env = GDALEnv()
+        _env.update_config_options(**default_options)
         log.debug(
             "New GDAL environment %r created", _env)
 
diff --git a/rasterio/warp.py b/rasterio/warp.py
--- a/rasterio/warp.py
+++ b/rasterio/warp.py
@@ -320,7 +320,7 @@ def calculate_default_transform(
 
     Note
     ----
-    Should be called within a raster.env.Env() context
+    Should be called within a rasterio.Env() context
 
     Some behavior of this function is determined by the
     CHECK_WITH_INVERT_PROJ environment variable


Test patch:
diff --git a/tests/test_env.py b/tests/test_env.py
--- a/tests/test_env.py
+++ b/tests/test_env.py
@@ -12,6 +12,7 @@
 from rasterio._drivers import (del_gdal_config, get_gdal_config,
                                set_gdal_config)
 from rasterio.env import defenv, delenv, getenv, setenv, ensure_env
+from rasterio.env import default_options
 from rasterio.errors import EnvError
 from rasterio.rio.main import main_group
 
@@ -48,7 +49,10 @@ def test_env_accessors(gdalenv):
     """High level GDAL env access."""
     defenv()
     setenv(foo='1', bar='2')
-    assert getenv() == rasterio.env._env.options == {'foo': '1', 'bar': '2'}
+    expected = default_options.copy()
+    expected.update({'foo': '1', 'bar': '2'})
+    assert getenv() == rasterio.env._env.options
+    assert getenv() == expected
     assert get_gdal_config('foo') == '1'
     assert get_gdal_config('bar') == '2'
     delenv()
@@ -84,10 +88,13 @@ def test_env_options(gdalenv):
     env = rasterio.Env(foo='x')
     assert env.options == {'foo': 'x'}
     assert not env.previous_options
-    assert getenv() == rasterio.env._env.options == {}
+    expected = default_options.copy()
+    assert getenv() == rasterio.env._env.options == expected
+    expected.update({'foo': 'x'})
     with env:
-        assert getenv() == rasterio.env._env.options == {'foo': 'x'}
-    assert getenv() == rasterio.env._env.options == {}
+        assert getenv() == rasterio.env._env.options == expected
+    del expected['foo']
+    assert getenv() == rasterio.env._env.options == expected
 
 
 def test_aws_session(gdalenv):
@@ -108,22 +115,26 @@ def test_aws_session_credentials(gdalenv):
         aws_access_key_id='id', aws_secret_access_key='key',
         aws_session_token='token', region_name='null-island-1')
     s = rasterio.env.Env(aws_session=aws_session)
-    assert getenv() == rasterio.env._env.options == {}
+    expected = default_options.copy()
+    assert getenv() == rasterio.env._env.options == expected
     s.get_aws_credentials()
-    assert getenv() == rasterio.env._env.options == {
+    expected.update({
         'aws_access_key_id': 'id', 'aws_region': 'null-island-1',
-        'aws_secret_access_key': 'key', 'aws_session_token': 'token'}
+        'aws_secret_access_key': 'key', 'aws_session_token': 'token'})
+    assert getenv() == rasterio.env._env.options == expected
 
 
 def test_with_aws_session_credentials(gdalenv):
     """Create an Env with a boto3 session."""
     with rasterio.Env(aws_access_key_id='id', aws_secret_access_key='key',
              aws_session_token='token', region_name='null-island-1') as s:
-        assert getenv() == rasterio.env._env.options == {}
+        expected = default_options.copy()
+        assert getenv() == rasterio.env._env.options == expected
         s.get_aws_credentials()
-        assert getenv() == rasterio.env._env.options == {
+        expected.update({
             'aws_access_key_id': 'id', 'aws_region': 'null-island-1',
-            'aws_secret_access_key': 'key', 'aws_session_token': 'token'}
+            'aws_secret_access_key': 'key', 'aws_session_token': 'token'})
+        assert getenv() == rasterio.env._env.options == expected
 
 
 def test_session_env_lazy(monkeypatch, gdalenv):
diff --git a/tests/test_warp.py b/tests/test_warp.py
--- a/tests/test_warp.py
+++ b/tests/test_warp.py
@@ -4,6 +4,7 @@
 import pytest
 from affine import Affine
 import numpy as np
+from packaging.version import parse
 
 import rasterio
 from rasterio.enums import Resampling
@@ -18,6 +19,27 @@
 DST_TRANSFORM = Affine.from_gdal(-8789636.708, 300.0, 0.0, 2943560.235, 0.0, -300.0)
 
 
+def supported_resampling(method):
+    if method == Resampling.gauss:
+        return False
+    gdal110plus_only = (
+        Resampling.mode, Resampling.average)
+    gdal2plus_only = (
+        Resampling.max, Resampling.min, Resampling.med,
+        Resampling.q1, Resampling.q3)
+    version = parse(rasterio.__gdal_version__)
+    if version < parse('1.10'):
+        return method not in gdal2plus_only and method not in gdal110plus_only
+    if version < parse('2.0'):
+        return method not in gdal2plus_only
+    return True
+
+
+reproj_expected = (
+    ({'CHECK_WITH_INVERT_PROJ': False}, 6215),
+    ({'CHECK_WITH_INVERT_PROJ': True}, 4005))
+
+
 class ReprojectParams(object):
     """Class to assist testing reprojection by encapsulating parameters."""
 
@@ -254,11 +276,12 @@ def test_reproject_out_of_bounds():
         assert not out.any()
 
 
-def test_reproject_nodata():
+@pytest.mark.parametrize("options, expected", reproj_expected)
+def test_reproject_nodata(options, expected):
     params = default_reproject_params()
     nodata = 215
 
-    with rasterio.Env():
+    with rasterio.Env(**options):
         source = np.ones((params.width, params.height), dtype=np.uint8)
         out = np.zeros((params.dst_width, params.dst_height),
                           dtype=source.dtype)
@@ -275,15 +298,16 @@ def test_reproject_nodata():
             dst_nodata=nodata
         )
 
-        assert (out == 1).sum() == 6215
+        assert (out == 1).sum() == expected
         assert (out == nodata).sum() == (params.dst_width *
-                                         params.dst_height - 6215)
+                                         params.dst_height - expected)
 
 
-def test_reproject_nodata_nan():
+@pytest.mark.parametrize("options, expected", reproj_expected)
+def test_reproject_nodata_nan(options, expected):
     params = default_reproject_params()
 
-    with rasterio.Env():
+    with rasterio.Env(**options):
         source = np.ones((params.width, params.height), dtype=np.float32)
         out = np.zeros((params.dst_width, params.dst_height),
                           dtype=source.dtype)
@@ -300,16 +324,17 @@ def test_reproject_nodata_nan():
             dst_nodata=np.nan
         )
 
-        assert (out == 1).sum() == 6215
+        assert (out == 1).sum() == expected
         assert np.isnan(out).sum() == (params.dst_width *
-                                       params.dst_height - 6215)
+                                       params.dst_height - expected)
 
 
-def test_reproject_dst_nodata_default():
+@pytest.mark.parametrize("options, expected", reproj_expected)
+def test_reproject_dst_nodata_default(options, expected):
     """If nodata is not provided, destination will be filled with 0."""
     params = default_reproject_params()
 
-    with rasterio.Env():
+    with rasterio.Env(**options):
         source = np.ones((params.width, params.height), dtype=np.uint8)
         out = np.zeros((params.dst_width, params.dst_height),
                           dtype=source.dtype)
@@ -324,9 +349,9 @@ def test_reproject_dst_nodata_default():
             dst_crs=params.dst_crs
         )
 
-        assert (out == 1).sum() == 6215
+        assert (out == 1).sum() == expected
         assert (out == 0).sum() == (params.dst_width *
-                                    params.dst_height - 6215)
+                                    params.dst_height - expected)
 
 
 def test_reproject_invalid_dst_nodata():
@@ -597,13 +622,15 @@ def test_reproject_unsupported_resampling_guass():
                 resampling=Resampling.gauss)
 
 
-@pytest.mark.xfail()
 @pytest.mark.parametrize("method", Resampling)
-def test_resample_no_invert_proj(method):
-    """Nearest and bilinear should produce valid results with
-    CHECK_WITH_INVERT_PROJ = False
+def test_resample_default_invert_proj(method):
+    """Nearest and bilinear should produce valid results
+    with the default Env
     """
-    with rasterio.Env(CHECK_WITH_INVERT_PROJ=False):
+    if not supported_resampling(method):
+        pytest.skip()
+
+    with rasterio.Env():
         with rasterio.open('tests/data/world.rgb.tif') as src:
             source = src.read(1)
             profile = src.profile.copy()
@@ -619,7 +646,7 @@ def test_resample_no_invert_proj(method):
 
         out = np.empty(shape=(dst_height, dst_width), dtype=np.uint8)
 
-        # nearest works fine
+        out = np.empty(src.shape, dtype=np.uint8)
         reproject(
             source,
             out,
@@ -627,11 +654,37 @@ def test_resample_no_invert_proj(method):
             src_crs=src.crs,
             dst_transform=dst_affine,
             dst_crs=dst_crs,
-            resampling=Resampling.nearest)
+            resampling=method)
 
         assert out.mean() > 0
 
-        # some other methods succeed but produce blank images
+
+@pytest.mark.xfail()
+@pytest.mark.parametrize("method", Resampling)
+def test_resample_no_invert_proj(method):
+    """Nearest and bilinear should produce valid results with
+    CHECK_WITH_INVERT_PROJ = False
+    """
+    if not supported_resampling(method):
+        pytest.skip()
+
+    with rasterio.Env(CHECK_WITH_INVERT_PROJ=False):
+        with rasterio.open('tests/data/world.rgb.tif') as src:
+            source = src.read(1)
+            profile = src.profile.copy()
+
+        dst_crs = {'init': 'EPSG:32619'}
+
+        # Calculate the ideal dimensions and transformation in the new crs
+        dst_affine, dst_width, dst_height = calculate_default_transform(
+            src.crs, dst_crs, src.width, src.height, *src.bounds)
+
+        profile['height'] = dst_height
+        profile['width'] = dst_width
+
+        out = np.empty(shape=(dst_height, dst_width), dtype=np.uint8)
+
+        # see #614, some resamplin methods succeed but produce blank images
         out = np.empty(src.shape, dtype=np.uint8)
         reproject(
             source,