Repository: rasterio/rasterio

Issue:
Warp issue reprojecting from EPSG:4326 to EPSG:3857 on global datasets
Hello, I've run into an issue trying to warp global raster datasets from EPSG:4326 to EPSG:3857.

Example dataset: [Natural Earth II with Shaded Relief](http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/50m/raster/NE2_50M_SR.zip)

```
rio warp --dst-crs EPSG:3857 NE2_50M_SR.tif NE2_50M_SR_3857.tif
```

```
Traceback (most recent call last):
  File "/Users/mbertrand/.virtualenvs/rasterio/bin/rio", line 9, in <module>
    load_entry_point('rasterio==0.31.0', 'console_scripts', 'rio')()
  File "/Users/mbertrand/.virtualenvs/rasterio/lib/python2.7/site-packages/click/core.py", line 716, in __call__
    return self.main(*args, **kwargs)
  File "/Users/mbertrand/.virtualenvs/rasterio/lib/python2.7/site-packages/click/core.py", line 696, in main
    rv = self.invoke(ctx)
  File "/Users/mbertrand/.virtualenvs/rasterio/lib/python2.7/site-packages/click/core.py", line 1060, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/Users/mbertrand/.virtualenvs/rasterio/lib/python2.7/site-packages/click/core.py", line 889, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/Users/mbertrand/.virtualenvs/rasterio/lib/python2.7/site-packages/click/core.py", line 534, in invoke
    return callback(*args, **kwargs)
  File "/Users/mbertrand/.virtualenvs/rasterio/lib/python2.7/site-packages/click/decorators.py", line 17, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "/Users/mbertrand/dev/rasterio/rasterio/rio/warp.py", line 195, in warp
    resolution=res)
  File "/Users/mbertrand/dev/rasterio/rasterio/warp.py", line 311, in calculate_default_transform
    dst_width = max(int(ceil(x_dif / resolution[0])), 1)
ValueError: cannot convert float NaN to integer
```

I get the same error when trying to reproject other global datasets from 4326 to 3857.

In [warp.transform_bounds](https://github.com/mapbox/rasterio/blob/master/rasterio/warp.py#L150), inf values are being returned.



Discussion:
Yes, I started to see this recently, too. rio-warp does not (yet) do the same final check gdalwarp makes to ensure that destination bounds are within the limits of a projection. We're going to fix this before 1.0, for sure.


Resolving patch:
diff --git a/rasterio/warp.py b/rasterio/warp.py
--- a/rasterio/warp.py
+++ b/rasterio/warp.py
@@ -1,6 +1,7 @@
 """Raster warping and reprojection"""
 
 from __future__ import absolute_import
+from __future__ import division
 
 from math import ceil
 import warnings
@@ -8,8 +9,10 @@
 from affine import Affine
 import numpy as np
 
+import rasterio
 from rasterio._base import _transform
-from rasterio._warp import _transform_geom, _reproject, Resampling
+from rasterio._warp import (_transform_geom, _reproject, Resampling,
+                            _calculate_default_transform)
 from rasterio.transform import guard_transform
 
 
@@ -250,8 +253,7 @@ def calculate_default_transform(
         bottom,
         right,
         top,
-        resolution=None,
-        densify_pts=21):
+        resolution=None):
     """
     Transforms bounds to destination coordinate system, calculates resolution
     if not provided, and returns destination transform and dimensions.
@@ -259,13 +261,8 @@ def calculate_default_transform(
 
     Destination transform is anchored from the left, top coordinate.
 
-    Destination width and height are calculated from the number of pixels on
-    each dimension required to fit the destination bounds.
-
-    If resolution is not provided, it is calculated using a weighted average
-    of the relative sizes of source width and height compared to the transformed
-    bounds (pixels are assumed to be square).
-
+    Destination width and height (and resolution if not provided), are
+    calculated using GDAL's method for suggest warp output.
 
     Parameters
     ----------
@@ -282,37 +279,41 @@ def calculate_default_transform(
         Bounding coordinates in src_crs, from the bounds property of a raster.
     resolution: tuple (x resolution, y resolution) or float, optional
         Target resolution, in units of target coordinate reference system.
-    densify_pts: uint, optional
-        Number of points to add to each edge to account for nonlinear
-        edges produced by the transform process.  Large numbers will produce
-        worse performance.  Default: 21 (gdal default).
 
     Returns
     -------
     tuple of destination affine transform, width, and height
     """
-
-    xmin, ymin, xmax, ymax = transform_bounds(
-        src_crs, dst_crs, left, bottom, right, top, densify_pts)
-
-    x_dif = xmax - xmin
-    y_dif = ymax - ymin
-    size = float(width + height)
-
-    if resolution is None:
-        # TODO: compare to gdalwarp default (see 
-        # _calculate_default_transform() in _warp.pyx.
-        avg_resolution = (
-            (x_dif / float(width)) * (float(width) / size) +
-            (y_dif / float(height)) * (float(height) / size)
-        )
-        resolution = (avg_resolution, avg_resolution)
-
-    elif not isinstance(resolution, (tuple, list)):
-        resolution = (resolution, resolution)
-
-    dst_affine = Affine(resolution[0], 0, xmin, 0, -resolution[1], ymax)
-    dst_width = max(int(ceil(x_dif / resolution[0])), 1)
-    dst_height = max(int(ceil(y_dif / resolution[1])), 1)
+    with rasterio.drivers():
+        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
+    # adjust the width/height by the ratio of estimated:specified res (ceil'd)
+    if resolution:
+
+        # resolutions argument into tuple
+        try:
+            res = (float(resolution), float(resolution))
+        except TypeError:
+            res = (resolution[0], resolution[0]) \
+                if len(resolution) == 1 else resolution[0:2]
+
+        # Assume yres is provided as positive,
+        # needs to be negative for north-up affine
+        xres = res[0]
+        yres = -res[1]
+
+        xratio = dst_affine.a / xres
+        yratio = dst_affine.e / yres
+
+        dst_affine = Affine(xres, dst_affine.b, dst_affine.c,
+                            dst_affine.d, yres, dst_affine.f)
+
+        dst_width = ceil(dst_width * xratio)
+        dst_height = ceil(dst_height * yratio)
 
     return dst_affine, dst_width, dst_height


Test patch:
diff --git a/tests/data/world.byte.tif b/tests/data/world.byte.tif
new file mode 100644
Binary files /dev/null and b/tests/data/world.byte.tif differ
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
@@ -114,11 +114,12 @@ def test_warp_reproject_dst_crs(runner, tmpdir):
         with rasterio.open(outputname) as output:
             assert output.count == src.count
             assert output.crs == {'init': 'epsg:4326'}
-            assert output.width == 824
-            assert output.height == 686
+            assert output.width == 835
+            assert output.height == 696
             assert numpy.allclose(output.bounds,
-                                  [-78.95864996545055, 23.564424693996177,
-                                   -76.57259451863895, 25.550873767433984])
+                                  [-78.95864996545055, 23.564787976164418,
+                                   -76.5759177302349, 25.550873767433984])
+
 
 def test_warp_reproject_dst_crs_error(runner, tmpdir):
     srcname = 'tests/data/RGB.byte.tif'
@@ -301,3 +302,18 @@ def test_warp_reproject_like(runner, tmpdir):
         assert numpy.allclose([0.001, 0.001], [output.affine.a, -output.affine.e])
         assert output.width == 10
         assert output.height == 10
+
+
+def test_warp_reproject_nolostdata(runner, tmpdir):
+    srcname = 'tests/data/world.byte.tif'
+    outputname = str(tmpdir.join('test.tif'))
+    result = runner.invoke(warp.warp, [srcname, outputname,
+                                       '--dst-crs', 'EPSG:3857'])
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+
+    with rasterio.open(outputname) as output:
+        arr = output.read()
+        # 50 column swath on the right edge should have some ones (gdalwarped has 7223)
+        assert arr[0, :, -50:].sum() > 7000
+        assert output.crs == {'init': 'epsg:3857'}
diff --git a/tests/test_warp.py b/tests/test_warp.py
--- a/tests/test_warp.py
+++ b/tests/test_warp.py
@@ -131,9 +131,9 @@ def test_transform_bounds_densify_out_of_bounds():
 
 def test_calculate_default_transform():
     target_transform = Affine(
-        0.0028956983577810586, 0.0, -78.95864996545055,
-        0.0, -0.0028956983577810586, 25.550873767433984
-    )
+        0.0028535715391804096, 0.0, -78.95864996545055,
+        0.0, -0.0028535715391804096, 25.550873767433984)
+
     with rasterio.drivers():
         with rasterio.open('tests/data/RGB.byte.tif') as src:
             wgs84_crs = {'init': 'EPSG:4326'}
@@ -141,8 +141,8 @@ def test_calculate_default_transform():
                 src.crs, wgs84_crs, src.width, src.height, *src.bounds)
 
             assert dst_transform.almost_equals(target_transform)
-            assert width == 824
-            assert height == 686
+            assert width == 835
+            assert height == 696
 
 
 def test_calculate_default_transform_single_resolution():
@@ -270,9 +270,9 @@ def test_reproject_nodata():
             dst_nodata=nodata
         )
 
-        assert (out == 1).sum() == 4461
+        assert (out == 1).sum() == 6215
         assert (out == nodata).sum() == (params.dst_width *
-                                         params.dst_height - 4461)
+                                         params.dst_height - 6215)
 
 
 def test_reproject_nodata_nan():
@@ -296,9 +296,9 @@ def test_reproject_nodata_nan():
             dst_nodata=numpy.nan
         )
 
-        assert (out == 1).sum() == 4461
+        assert (out == 1).sum() == 6215
         assert numpy.isnan(out).sum() == (params.dst_width *
-                                         params.dst_height - 4461)
+                                         params.dst_height - 6215)
 
 
 
@@ -325,9 +325,9 @@ def test_reproject_dst_nodata_default():
             dst_crs=params.dst_crs
         )
 
-        assert (out == 1).sum() == 4461
+        assert (out == 1).sum() == 6215
         assert (out == 0).sum() == (params.dst_width *
-                                    params.dst_height - 4461)
+                                    params.dst_height - 6215)
 
 
 def test_reproject_invalid_dst_nodata():