Repository: rasterio/rasterio

Issue:
rio bounds results in incorrect bounds
`rio bounds` is giving me an incorrect extent:

![image](https://cloud.githubusercontent.com/assets/5084513/12737662/559d4ef4-c90f-11e5-90d1-a8cfc35b7982.png)

```
[78.19825215393689, 27.874667692983305, 78.22262314299678, 27.897332159267222]
```

Interestingly, the min/max x bounds seem to cut off at the _min_ extent of the data in the positive and negative direction, rather than the maximum extent. The `y` dimension looks correct. Here is the `rio info`'d output from the same file:

```
{
  "count": 3, 
  "crs": "EPSG:32644", 
  "interleave": "band", 
  "dtype": "uint8", 
  "driver": "GTiff", 
  "transform": [
    0.3, 
    0.0, 
    224144.7, 
    0.0, 
    -0.3, 
    3088931.4
  ], 
  "lnglat": [
    78.21043638215713, 
    27.886000381669582
  ], 
  "height": 8192, 
  "width": 8192, 
  "shape": [
    8192, 
    8192
  ], 
  "tiled": false, 
  "res": [
    0.3, 
    0.3
  ], 
  "nodata": null, 
  "bounds": [
    224144.7, 
    3086473.8, 
    226602.30000000002, 
    3088931.4
  ]
}
```

@sgillies has this above input file.



Discussion:
Additional note: When running `rio bounds --projected`, (`UTM zone 44N`) then projecting the bbox on the fly, the bounds line up correctly (red):

![image](https://cloud.githubusercontent.com/assets/5084513/12738010/09d082b8-c912-11e5-971f-5120e68e2622.png)

I'm not sure, but maybe this is related to the way the extrema are calculated and projected using `pyproj`?

@dnomadb right: `rio bounds --projected`, in the raster's coordinate reference system, is square with the image. `rio bounds --geographic` returns the SW and NE corners, transformed to WGS84, but not necessarily the lng/lat minimum bounding rectangle.

Would you like a command line option to get the lng/lat minimum bounding rectangle? Have that be the default instead of what we have now?

Thoughts on this @brendan-ward @geowurster @perrygeo?

Ah @sgillies so bottom line is that `--geographic` only unprojects the bounds from the native projection itself - there is no logic in how these are handled upon projection.

For now I switched to using the alpha band's footprint via `rio shapes --bidx 4 --as-mask <input>` + `fio shapes`, but it seems that:

> get the lng/lat minimum bounding rectangle

Would be useful? Yes, I think so. I think that many others would make the same mistake that I have, and assume that the the `--geographic` option would return the "entire projected bounds" , aka `minimum bounding rectangle`.

@sgillies my preference would be for the default option to return the minimum bounding rectangle, with  densification along the edges of the input bounding box (default of 21), like we do [here](https://github.com/mapbox/rasterio/blob/master/rasterio/warp.py#L93).


Resolving patch:
diff --git a/rasterio/rio/features.py b/rasterio/rio/features.py
--- a/rasterio/rio/features.py
+++ b/rasterio/rio/features.py
@@ -18,6 +18,7 @@
 import rasterio
 from rasterio.transform import Affine
 from rasterio.coords import disjoint_bounds
+from rasterio.warp import transform_bounds
 
 logger = logging.getLogger('rio')
 
@@ -641,7 +642,7 @@ def feature_value(feature):
 @click.command(short_help="Write bounding boxes to stdout as GeoJSON.")
 # One or more files, the bounds of each are a feature in the collection
 # object or feature sequence.
-@click.argument('INPUT', nargs=-1, type=click.Path(exists=True))
+@click.argument('INPUT', nargs=-1, type=click.Path(exists=True), required=True)
 @precision_opt
 @indent_opt
 @compact_opt
@@ -692,22 +693,20 @@ def __call__(self):
             for i, path in enumerate(input):
                 with rasterio.open(path) as src:
                     bounds = src.bounds
-                    xs = [bounds[0], bounds[2]]
-                    ys = [bounds[1], bounds[3]]
                     if dst_crs:
-                        xs, ys = rasterio.warp.transform(
-                            src.crs, {'init': dst_crs}, xs, ys)
+                        bbox = transform_bounds(src.crs,
+                                                dst_crs, *bounds)
                     elif projection == 'mercator':
-                        xs, ys = rasterio.warp.transform(
-                            src.crs, {'init': 'epsg:3857'}, xs, ys)
+                        bbox = transform_bounds(src.crs,
+                                                {'init': 'epsg:3857'}, *bounds)
                     elif projection == 'geographic':
-                        xs, ys = rasterio.warp.transform(
-                            src.crs, {'init': 'epsg:4326'}, xs, ys)
+                        bbox = transform_bounds(src.crs,
+                                                {'init': 'epsg:4326'}, *bounds)
+                    else:
+                        bbox = bounds
 
                 if precision >= 0:
-                    xs = [round(v, precision) for v in xs]
-                    ys = [round(v, precision) for v in ys]
-                bbox = [min(xs), min(ys), max(xs), max(ys)]
+                    bbox = [round(b, precision) for b in bbox]
 
                 yield {
                     'type': 'Feature',
@@ -715,11 +714,11 @@ def __call__(self):
                     'geometry': {
                         'type': 'Polygon',
                         'coordinates': [[
-                            [xs[0], ys[0]],
-                            [xs[1], ys[0]],
-                            [xs[1], ys[1]],
-                            [xs[0], ys[1]],
-                            [xs[0], ys[0]] ]]},
+                            [bbox[0], bbox[1]],
+                            [bbox[2], bbox[1]],
+                            [bbox[2], bbox[3]],
+                            [bbox[0], bbox[3]],
+                            [bbox[0], bbox[1]]]]},
                     'properties': {
                         'id': str(i),
                         'title': path,


Test patch:
diff --git a/tests/test_rio_info.py b/tests/test_rio_info.py
--- a/tests/test_rio_info.py
+++ b/tests/test_rio_info.py
@@ -482,7 +482,7 @@ def test_bounds_obj_bbox():
         '--precision', '2'
     ])
     assert result.exit_code == 0
-    assert result.output.strip() == '[-78.9, 23.56, -76.6, 25.55]'
+    assert result.output.strip() == '[-78.96, 23.56, -76.57, 25.55]'
 
 
 def test_bounds_compact():
@@ -495,7 +495,7 @@ def test_bounds_compact():
         '--compact'
     ])
     assert result.exit_code == 0
-    assert result.output.strip() == '[-78.9,23.56,-76.6,25.55]'
+    assert result.output.strip() == '[-78.96,23.56,-76.57,25.55]'
 
 
 def test_bounds_indent():
@@ -522,7 +522,7 @@ def test_bounds_obj_bbox_mercator():
     ])
     assert result.exit_code == 0
     assert result.output.strip() == (
-        '[-8782900.033, 2700489.278, -8527010.472, 2943560.235]')
+        '[-8789636.708, 2700489.278, -8524281.514, 2943560.235]')
 
 
 def test_bounds_obj_bbox_projected():
@@ -574,7 +574,7 @@ def test_bounds_seq():
     ])
     assert result.exit_code == 0
     assert result.output == (
-        '[-78.9, 23.56, -76.6, 25.55]\n[-78.9, 23.56, -76.6, 25.55]\n')
+        '[-78.96, 23.56, -76.57, 25.55]\n[-78.96, 23.56, -76.57, 25.55]\n')
     assert '\x1e' not in result.output
 
 
@@ -591,8 +591,7 @@ def test_bounds_seq_rs():
     ])
     assert result.exit_code == 0
     assert result.output == (
-        '\x1e[-78.9, 23.56, -76.6, 25.55]\n\x1e[-78.9, 23.56, -76.6, 25.55]\n')
-
+        '\x1e[-78.96, 23.56, -76.57, 25.55]\n\x1e[-78.96, 23.56, -76.57, 25.55]\n')
 
 def test_insp():
     runner = CliRunner()