[rasterio] 01/03: Imported Upstream version 0.17

Johan Van de Wauw johanvdw-guest at moszumanska.debian.org
Thu Jan 15 21:54:20 UTC 2015


This is an automated email from the git hooks/post-receive script.

johanvdw-guest pushed a commit to branch master
in repository rasterio.

commit f460b6c2e07202fd3ee327d75c7fa1da20ce8ef8
Author: Johan Van de Wauw <johan.vandewauw at gmail.com>
Date:   Thu Jan 15 22:34:48 2015 +0100

    Imported Upstream version 0.17
---
 .travis.yml                  |   3 +-
 CHANGES.txt                  |   7 ++
 README.rst                   |  60 ++++++-----
 build-wheels.sh              |  18 ++++
 docs/cli.rst                 |  12 +--
 rasterio/__init__.py         |   2 +-
 rasterio/_base.pyx           |   5 +-
 rasterio/_drivers.pyx        |  14 ++-
 rasterio/_io.pyx             | 221 +++++++++++++++++++++++++++-----------
 rasterio/rio/bands.py        |  20 ++--
 rasterio/rio/cli.py          |  41 +++----
 rasterio/rio/features.py     |  82 ++++++--------
 rasterio/rio/info.py         |   3 +-
 rasterio/rio/merge.py        | 127 +++++++++++++++++-----
 rasterio/rio/options.py      |  21 ----
 rasterio/rio/rio.py          |  76 +++++--------
 rasterio/tool.py             |  61 ++++++-----
 requirements-dev.txt         |  13 ++-
 requirements.txt             |   6 +-
 setup.py                     | 106 +++++++++++++------
 tests/test_cli.py            | 115 --------------------
 tests/test_indexing.py       |  10 +-
 tests/test_read.py           |  13 ---
 tests/test_read_boundless.py |  51 +++++++++
 tests/test_read_resample.py  |  33 ++++++
 tests/test_rio_bands.py      |  12 +--
 tests/test_rio_features.py   |  99 +++++++++++++++++
 tests/test_rio_info.py       |  18 +++-
 tests/test_rio_merge.py      | 247 +++++++++++++++++++++++++++++++++++++++++++
 tests/test_rio_options.py    |  15 ---
 tests/test_rio_rio.py        |  38 ++++---
 tests/test_tool.py           |  44 ++++++++
 32 files changed, 1073 insertions(+), 520 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 0fc4c5c..97cb081 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -14,7 +14,6 @@ install:
   - "pip install coveralls"
   - "pip install -e ."
 script: 
-  - py.test
-  - coverage run --source=rasterio --omit='*.pxd,*.pyx,*/tests/*,*/docs/*,*/examples/*,*/benchmarks/*' -m py.test
+  - coverage run --source=rasterio --omit='*.pxd,*.pyx,*/tests/*,*/docs/*,*/examples/*,*/benchmarks/*,*/rio/main.py,*/rio/__init__.py' -m py.test
 after_success:
   - coveralls
diff --git a/CHANGES.txt b/CHANGES.txt
index 6830152..fc51402 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,6 +1,13 @@
 Changes
 =======
 
+0.17.0 (2015-01-15)
+-------------------
+- Enhancements to rio-merge: relaxation of same-extent and same-resolution
+  constraints, addition of --bounds and --res options (#242, 247).
+- Data files in support of binary wheels (#239).
+- Fix for reading bands with undefined nodata (#237, #240).
+
 0.16.0 (2014-12-16)
 -------------------
 - More graceful, slice-like handling of windows (#191).
diff --git a/README.rst b/README.rst
index 067dc11..14dfc7e 100644
--- a/README.rst
+++ b/README.rst
@@ -14,7 +14,7 @@ Rasterio employs GDAL under the hood for file I/O and raster formatting. Its
 functions typically accept and return Numpy ndarrays. Rasterio is designed to
 make working with geospatial raster data more productive and more fun.
 
-Rasterio is pronounced raw-STIER-ee-oh.
+Rasterio is pronounced raw-STEER-ee-oh.
 
 Example
 =======
@@ -164,36 +164,48 @@ using Python.
     >>> b.min(), b.max(), b.mean()
     (1, 255, 44.434478650699106)
 
-Dependencies
+Installation
 ============
 
-C library dependecies:
+Dependencies
+------------
 
-- GDAL 1.9+
+Rasterio has one C library dependency: GDAL >=1.9. GDAL itself depends on a
+number of other libraries provided by most major operating systems and also
+depends on the non standard GEOS and PROJ4 libraries.
 
-Python package dependencies (see also requirements.txt):
+Python package dependencies (see also requirements.txt): affine, cligj (and
+click), enum34, numpy.
 
-- affine
-- Numpy
-- setuptools
+Development also requires (see requirements-dev.txt) Cython and other packages.
 
-Development also requires (see requirements-dev.txt)
+Rasterio binaries for OS X
+--------------------------
 
-- Cython
-- pytest
+Binary wheels with the GDAL, GEOS, and PROJ4 libraries included are available
+for OS X versions 10.7+ starting with Rasterio version 0.17. To install, just
+run ``pip install rasterio``. These binary wheels are preferred by newer
+versions of pip. If you don't want these wheels and want to install from
+a source distribution, run ``pip install rasterio --no-use-wheel`` instead.
 
-Installation
-============
+The included GDAL library is fairly minimal, providing only the format drivers
+that ship with GDAL and are enabled by default. To get access to more formats,
+you must build from a source distribution (see below).
 
-Rasterio is a C extension and to install on Linux or OS X you'll need a working
-compiler (XCode on OS X etc). You'll also need Numpy preinstalled; the Numpy
-headers are required to run the rasterio setup script. Numpy has to be
-installed (via the indicated requirements file) before rasterio can be
-installed. See rasterio's Travis `configuration
+Binary wheels for other operating systems will be available in a future
+release.
+
+Installing from the source distribution
+---------------------------------------
+
+Rasterio is a Python C extension and to build you'll need a working compiler
+(XCode on OS X etc). You'll also need Numpy preinstalled; the Numpy headers are
+required to run the rasterio setup script. Numpy has to be installed (via the
+indicated requirements file) before rasterio can be installed. See rasterio's
+Travis `configuration
 <https://github.com/mapbox/rasterio/blob/master/.travis.yml>`__ for more
 guidance.
 
-
 Linux
 -----
 
@@ -202,9 +214,8 @@ The following commands are adapted from Rasterio's Travis-CI configuration.
 .. code-block:: console
 
     $ sudo add-apt-repository ppa:ubuntugis/ppa
-    $ sudo apt-get update -qq
+    $ sudo apt-get update
     $ sudo apt-get install python-numpy libgdal1h gdal-bin libgdal-dev
-    $ pip install -r https://raw.githubusercontent.com/mapbox/rasterio/master/requirements.txt
     $ pip install rasterio
 
 Adapt them as necessary for your Linux system.
@@ -212,17 +223,13 @@ Adapt them as necessary for your Linux system.
 OS X
 ----
 
-Wheels are available on PyPI for Homebrew based Python environments.
+For a Homebrew based Python environment, do the following.
 
 .. code-block:: console
 
     $ brew install gdal
-    $ pip install -r https://raw.githubusercontent.com/mapbox/rasterio/master/requirements.txt
     $ pip install rasterio
 
-The wheels are incompatible with MacPorts. MacPorts users will need to specify
-a source installation instead: ``pip install --no-use-wheel``.
-
 Windows
 -------
 
@@ -257,4 +264,3 @@ Changes
 -------
 
 See CHANGES.txt
-
diff --git a/build-wheels.sh b/build-wheels.sh
new file mode 100644
index 0000000..36e68dc
--- /dev/null
+++ b/build-wheels.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+# Automation of this is a TODO. For now, it depends on manually built libraries
+# as detailed in https://gist.github.com/sgillies/a8a2fb910a98a8566d0a.
+
+export MACOSX_DEPLOYMENT_TARGET=10.6
+export GDAL_CONFIG="/usr/local/bin/gdal-config"
+export PACKAGE_DATA=1
+
+VERSION=$1
+
+source $HOME/envs/riowhl27/bin/activate
+CFLAGS="`$GDAL_CONFIG --cflags`" LDFLAGS="`$GDAL_CONFIG --libs` `$GDAL_CONFIG --dep-libs`" python setup.py bdist_wheel -d wheels/$VERSION
+source $HOME/envs/riowhl34/bin/activate
+CFLAGS="`$GDAL_CONFIG --cflags`" LDFLAGS="`$GDAL_CONFIG --libs` `$GDAL_CONFIG --dep-libs`" python setup.py bdist_wheel -d wheels/$VERSION
+
+parallel delocate-wheel -w fixed_wheels/$VERSION --require-archs=intel -v {} ::: wheels/$VERSION/*.whl
+parallel cp {} {.}.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl ::: fixed_wheels/$VERSION/*.whl
diff --git a/docs/cli.rst b/docs/cli.rst
index a31e0ff..a744c4a 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -136,12 +136,12 @@ The insp command opens a dataset and an interpreter.
 merge
 -----
 
-The merge command can be used to flatten a stack of identically layed out
+The merge command can be used to flatten a stack of identically structured
 datasets.
 
 .. code-block:: console
 
-    $ rio merge rasterio/tests/data/R*.tif -o result.tif
+    $ rio merge rasterio/tests/data/R*.tif merged.tif
 
 shapes
 ------
@@ -188,10 +188,10 @@ Examples using the Rasterio testing dataset that produce a copy of it.
 
 .. code-block:: console
 
-    $ rio stack RGB.byte.tif -o stacked.tif
-    $ rio stack RGB.byte.tif --bidx 1,2,3 -o stacked.tif
-    $ rio stack RGB.byte.tif --bidx 1..3 -o stacked.tif
-    $ rio stack RGB.byte.tif --bidx ..2 RGB.byte.tif --bidx 3.. -o stacked.tif
+    $ rio stack RGB.byte.tif stacked.tif
+    $ rio stack RGB.byte.tif --bidx 1,2,3 stacked.tif
+    $ rio stack RGB.byte.tif --bidx 1..3 stacked.tif
+    $ rio stack RGB.byte.tif --bidx ..2 RGB.byte.tif --bidx 3.. stacked.tif
 
 transform
 ---------
diff --git a/rasterio/__init__.py b/rasterio/__init__.py
index 4bbc8d0..531dae2 100644
--- a/rasterio/__init__.py
+++ b/rasterio/__init__.py
@@ -18,7 +18,7 @@ from rasterio.transform import Affine, guard_transform
 
 __all__ = [
     'band', 'open', 'drivers', 'copy', 'pad']
-__version__ = "0.16"
+__version__ = "0.17"
 
 log = logging.getLogger('rasterio')
 class NullHandler(logging.Handler):
diff --git a/rasterio/_base.pyx b/rasterio/_base.pyx
index e529efd..660d112 100644
--- a/rasterio/_base.pyx
+++ b/rasterio/_base.pyx
@@ -377,10 +377,7 @@ cdef class DatasetReader(object):
         """Returns the window corresponding to the world bounding box."""
         ul = self.index(left, top)
         lr = self.index(right, bottom)
-        if ul[0] < 0 or ul[1] < 0 or lr[0] > self.height or lr[1] > self.width:
-            raise ValueError("Bounding box overflows the dataset extents")
-        else:
-            return tuple(zip(ul, lr))
+        return tuple(zip(ul, lr))
 
     @property
     def meta(self):
diff --git a/rasterio/_drivers.pyx b/rasterio/_drivers.pyx
index 7d470e2..b8080da 100644
--- a/rasterio/_drivers.pyx
+++ b/rasterio/_drivers.pyx
@@ -95,9 +95,17 @@ cdef class GDALEnv(object):
             raise ValueError("Drivers not registered")
 
         if 'GDAL_DATA' not in os.environ:
-            datadir = os.path.join(sys.prefix, 'share/gdal')
-            if os.path.exists(os.path.join(datadir, 'pcs.csv')):
-                os.environ['GDAL_DATA'] = datadir
+            whl_datadir = os.path.abspath(
+                os.path.join(os.path.dirname(__file__), "gdal_data"))
+            share_datadir = os.path.join(sys.prefix, 'share/gdal')
+            if os.path.exists(os.path.join(whl_datadir, 'pcs.csv')):
+                os.environ['GDAL_DATA'] = whl_datadir
+            elif os.path.exists(os.path.join(share_datadir, 'pcs.csv')):
+                os.environ['GDAL_DATA'] = share_datadir
+        if 'PROJ_LIB' not in os.environ:
+            whl_datadir = os.path.abspath(
+                os.path.join(os.path.dirname(__file__), "proj_data"))
+            os.environ['PROJ_LIB'] = whl_datadir
 
         for key, val in self.options.items():
             key_b = key.upper().encode('utf-8')
diff --git a/rasterio/_io.pyx b/rasterio/_io.pyx
index 38edae0..98e18e1 100644
--- a/rasterio/_io.pyx
+++ b/rasterio/_io.pyx
@@ -38,7 +38,7 @@ else:
 # Single band IO functions.
 
 cdef int io_ubyte(
-        void *hband, 
+        void *hband,
         int mode,
         int xoff,
         int yoff,
@@ -51,7 +51,7 @@ cdef int io_ubyte(
             &buffer[0, 0], buffer.shape[1], buffer.shape[0], 1, 0, 0)
 
 cdef int io_uint16(
-        void *hband, 
+        void *hband,
         int mode,
         int xoff,
         int yoff,
@@ -547,31 +547,50 @@ cdef class RasterReader(_base.DatasetReader):
         """
         return self.read(bidx, out=out, window=window, masked=masked)
 
-    def read(self, indexes=None, out=None, window=None, masked=None):
-        """Read raster bands as a multidimensional array
 
-        If `indexes` is a list, the result is a 3D array, but
-        is a 2D array if it is a band index number.
-
-        Optional `out` argument is a reference to an output array with the
-        same dimensions and shape.
+    def read(self, indexes=None, out=None, window=None, masked=None,
+            boundless=False):
+        """Read raster bands as a multidimensional array
 
-        See `read_band` for usage of the optional `window` argument.
+        Parameters
+        ----------
+        indexes : list of ints or a single int, optional
+            If `indexes` is a list, the result is a 3D array, but is
+            a 2D array if it is a band index number.
+
+        out: numpy ndarray, optional
+            An optional reference to an output array with the same
+            dimensions and shape.
+
+        window : a pair (tuple) of pairs of ints, optional
+            The optional `window` argument is a 2 item tuple. The first
+            item is a tuple containing the indexes of the rows at which
+            the window starts and stops and the second is a tuple
+            containing the indexes of the columns at which the window
+            starts and stops. For example, ((0, 2), (0, 2)) defines
+            a 2x2 window at the upper left of the raster dataset.
+
+        masked : bool, optional
+            The return type will be either a regular NumPy array, or
+            a masked NumPy array depending on the `masked` argument. The
+            return type is forced if either `True` or `False`, but will
+            be chosen if `None`.  For `masked=None` (default), the array
+            will be the same type as `out` (if used), or will be masked
+            if any of the nodatavals are not `None`.
+
+        boundless : bool, optional (default `False`)
+            If `True`, windows that extend beyond the dataset's extent
+            are permitted and partially or completely filled arrays will
+            be returned as appropriate.
+
+        Returns
+        -------
+        Numpy ndarray
 
-        The return type will be either a regular NumPy array, or a masked
-        NumPy array depending on the `masked` argument. The return type is
-        forced if either `True` or `False`, but will be chosen if `None`.
-        For `masked=None` (default), the array will be the same type as
-        `out` (if used), or will be masked if any of the nodatavals are
-        not `None`.
         """
-        cdef int height, width, xoff, yoff, aix, bidx, indexes_count
-        cdef int retval = 0
-        return2d = False
 
-        if self._hds == NULL:
-            raise ValueError("can't read closed raster file")
-        if indexes is None:  # Default: read all bands
+        return2d = False
+        if indexes is None:
             indexes = self.indexes
         elif isinstance(indexes, int):
             indexes = [indexes]
@@ -580,6 +599,7 @@ cdef class RasterReader(_base.DatasetReader):
                 out.shape = (1,) + out.shape
         if not indexes:
             raise ValueError("No indexes to read")
+
         check_dtypes = set()
         nodatavals = []
         # Check each index before processing 3D array
@@ -589,44 +609,139 @@ cdef class RasterReader(_base.DatasetReader):
             idx = self.indexes.index(bidx)
             check_dtypes.add(self.dtypes[idx])
             nodatavals.append(self._nodatavals[idx])
+        # Mixed dtype reads are not supported at this time.
         if len(check_dtypes) > 1:
             raise ValueError("more than one 'dtype' found")
         elif len(check_dtypes) == 0:
             dtype = self.dtypes[0]
-        else:  # unique dtype; normal case
+        else:
             dtype = check_dtypes.pop()
 
-        # Windows are always limited to the dataset's extent.
+        # Get the natural shape of the read window, boundless or not.
+        win_shape = (len(indexes),)
         if window:
-            window = eval_window(window, self.height, self.width)
-            window = ((
-                    min(window[0][0] or 0, self.height),
-                    min(window[0][1] or self.height, self.height)), (
-                    min(window[1][0] or 0, self.width),
-                    min(window[1][1] or self.width, self.width)))
-
-        out_shape = (len(indexes),) + (
-            window
-            and window_shape(window, self.height, self.width)
-            or self.shape)
+            if boundless:
+                win_shape += (
+                        window[0][1]-window[0][0], window[1][1]-window[1][0])
+            else:
+                w = eval_window(window, self.height, self.width)
+                minr = min(max(w[0][0], 0), self.height)
+                maxr = max(0, min(w[0][1], self.height))
+                minc = min(max(w[1][0], 0), self.width)
+                maxc = max(0, min(w[1][1], self.width))
+                win_shape += (maxr - minr, maxc - minc)
+                window = ((minr, maxr), (minc, maxc))
+        else:
+            win_shape += self.shape
+
         if out is not None:
             if out.dtype != dtype:
                 raise ValueError(
                     "the array's dtype '%s' does not match "
                     "the file's dtype '%s'" % (out.dtype, dtype))
-            if out.shape[0] != out_shape[0]:
+            if out.shape[0] != win_shape[0]:
                 raise ValueError(
-                    "'out' shape %s does not mach raster slice shape %s" %
-                    (out.shape, out_shape))
+                    "'out' shape %s does not match window shape %s" %
+                    (out.shape, win_shape))
             if masked is None:
                 masked = hasattr(out, 'mask')
         if masked is None:
             masked = any([x is not None for x in nodatavals])
         if out is None:
-            if masked:
-                out = np.ma.empty(out_shape, dtype)
+            out = np.zeros(win_shape, dtype)
+            for ndv, arr in zip(
+                    self.nodatavals, out if len(win_shape) == 3 else [out]):
+                if ndv is not None:
+                    arr.fill(ndv)
+
+        # We can jump straight to _read() in some cases. We can ignore
+        # the boundless flag if there's no given window.
+        if not boundless or not window:
+            out = self._read(indexes, out, window, dtype)
+
+        else:
+            # Compute the overlap between the dataset and the boundless window.
+            overlap = ((
+                max(min(window[0][0] or 0, self.height), 0),
+                max(min(window[0][1] or self.height, self.height), 0)), (
+                max(min(window[1][0] or 0, self.width), 0),
+                max(min(window[1][1] or self.width, self.width), 0)))
+
+            if overlap != ((0, 0), (0, 0)):
+                # Prepare a buffer.
+                window_h, window_w = win_shape[-2:]
+                overlap_h = overlap[0][1] - overlap[0][0]
+                overlap_w = overlap[1][1] - overlap[1][0]
+                scaling_h = float(out.shape[-2:][0])/window_h
+                scaling_w = float(out.shape[-2:][1])/window_w
+                buffer_shape = (int(overlap_h*scaling_h), int(overlap_w*scaling_w))
+                data = np.empty(win_shape[:-2] + buffer_shape, dtype)
+                data = self._read(indexes, data, overlap, dtype)
+            else:
+                data = None
+
+            if data is not None:
+                # Determine where to put the data in the output window.
+                data_h, data_w = data.shape[-2:]
+                roff = 0
+                coff = 0
+                if window[0][0] < 0:
+                    roff = int(window_h*scaling_h) - data_h
+                if window[1][0] < 0:
+                    coff = int(window_w*scaling_w) - data_w
+                for dst, src in zip(
+                        out if len(out.shape) == 3 else [out],
+                        data if len(data.shape) == 3 else [data]):
+                    dst[roff:roff+data_h, coff:coff+data_w] = src
+
+        # Masking the output. TODO: explain the logic better.
+        if masked:
+            if len(set(nodatavals)) == 1:
+                if nodatavals[0] is None:
+                    out = np.ma.masked_array(out, copy=False)
+                elif np.isnan(nodatavals[0]):
+                    out = np.ma.masked_where(np.isnan(out), out, copy=False)
+                else:
+                    out = np.ma.masked_equal(out, nodatavals[0], copy=False)
             else:
-                out = np.empty(out_shape, dtype)
+                out = np.ma.masked_array(out, copy=False)
+                for aix in range(len(indexes)):
+                    if nodatavals[aix] is None:
+                        band_mask = False
+                    elif np.isnan(nodatavals[aix]):
+                        band_mask = np.isnan(out[aix])
+                    else:
+                        band_mask = out[aix] == nodatavals[aix]
+                    out[aix].mask = band_mask
+        if return2d:
+            out.shape = out.shape[1:]
+
+        return out
+
+
+    def _read(self, indexes, out, window, dtype):
+        """Read raster bands as a multidimensional array
+
+        If `indexes` is a list, the result is a 3D array, but
+        is a 2D array if it is a band index number.
+
+        Optional `out` argument is a reference to an output array with the
+        same dimensions and shape.
+
+        See `read_band` for usage of the optional `window` argument.
+
+        The return type will be either a regular NumPy array, or a masked
+        NumPy array depending on the `masked` argument. The return type is
+        forced if either `True` or `False`, but will be chosen if `None`.
+        For `masked=None` (default), the array will be the same type as
+        `out` (if used), or will be masked if any of the nodatavals are
+        not `None`.
+        """
+        cdef int height, width, xoff, yoff, aix, bidx, indexes_count
+        cdef int retval = 0
+
+        if self._hds == NULL:
+            raise ValueError("can't read closed raster file")
 
         # Prepare the IO window.
         if window:
@@ -696,31 +811,9 @@ cdef class RasterReader(_base.DatasetReader):
         elif retval == 4:
             raise ValueError("NULL band")
 
-        # Masking the output. TODO: explain the logic better.
-        if masked:
-            test1nodata = set(nodatavals)
-            if len(test1nodata) == 1:
-                if nodatavals[0] is None:
-                    out = np.ma.masked_array(out, copy=False)
-                elif np.isnan(nodatavals[0]):
-                    out = np.ma.masked_where(np.isnan(out), out, copy=False)
-                else:
-                    out = np.ma.masked_equal(out, nodatavals[0], copy=False)
-            else:
-                out = np.ma.masked_array(out, copy=False)
-                for aix in range(len(indexes)):
-                    if nodatavals[aix] is None:
-                        band_mask = False
-                    elif np.isnan(nodatavals[aix]):
-                        band_mask = np.isnan(out[aix])
-                    else:
-                        band_mask = out[aix] == nodatavals[aix]
-                    out[aix].mask = band_mask
-        
-        if return2d:
-            out.shape = out.shape[1:]
         return out
 
+
     def read_mask(self, out=None, window=None):
         """Read the mask band into an `out` array if provided, 
         otherwise return a new array containing the dataset's
diff --git a/rasterio/rio/bands.py b/rasterio/rio/bands.py
index cab2c7b..0350e27 100644
--- a/rasterio/rio/bands.py
+++ b/rasterio/rio/bands.py
@@ -3,6 +3,7 @@ import os.path
 import sys
 
 import click
+from cligj import files_inout_arg, format_opt
 
 import rasterio
 
@@ -23,20 +24,15 @@ PHOTOMETRIC_CHOICES = [val.lower() for val in [
 
 # Stack command.
 @cli.command(short_help="Stack a number of bands into a multiband dataset.")
- at click.argument('input', nargs=-1,
-                type=click.Path(exists=True, resolve_path=True), required=True)
+ at files_inout_arg
+ at format_opt
 @click.option('--bidx', multiple=True,
               help="Indexes of input file bands.")
 @click.option('--photometric', default=None,
               type=click.Choice(PHOTOMETRIC_CHOICES),
               help="Photometric interpretation")
- at click.option('-o','--output',
-              type=click.Path(exists=False, resolve_path=True), required=True,
-              help="Path to output file.")
- at click.option('-f', '--format', '--driver', default='GTiff',
-              help="Output format driver")
 @click.pass_context
-def stack(ctx, input, bidx, photometric, output, driver):
+def stack(ctx, files, driver, bidx, photometric):
     """Stack a number of bands from one or more input files into a
     multiband dataset.
 
@@ -74,9 +70,11 @@ def stack(ctx, input, bidx, photometric, output, driver):
     logger = logging.getLogger('rio')
     try:
         with rasterio.drivers(CPL_DEBUG=verbosity>2):
+            output = files[-1]
+            files = files[:-1]
             output_count = 0
             indexes = []
-            for path, item in zip_longest(input, bidx, fillvalue=None):
+            for path, item in zip_longest(files, bidx, fillvalue=None):
                 with rasterio.open(path) as src:
                     src_indexes = src.indexes
                 if item is None:
@@ -99,7 +97,7 @@ def stack(ctx, input, bidx, photometric, output, driver):
                         indexes.append(parts)
                         output_count += len(parts)
 
-            with rasterio.open(input[0]) as first:
+            with rasterio.open(files[0]) as first:
                 kwargs = first.meta
                 kwargs['transform'] = kwargs.pop('affine')
 
@@ -112,7 +110,7 @@ def stack(ctx, input, bidx, photometric, output, driver):
 
             with rasterio.open(output, 'w', **kwargs) as dst:
                 dst_idx = 1
-                for path, index in zip(input, indexes):
+                for path, index in zip(files, indexes):
                     with rasterio.open(path) as src:
                         if isinstance(index, int):
                             data = src.read(index)
diff --git a/rasterio/rio/cli.py b/rasterio/rio/cli.py
index cf54fda..37f2ff2 100644
--- a/rasterio/rio/cli.py
+++ b/rasterio/rio/cli.py
@@ -3,9 +3,10 @@ import logging
 import sys
 
 import click
+from cligj import verbose_opt, quiet_opt
 
 import rasterio
-from rasterio.rio import options
+
 
 def configure_logging(verbosity):
     log_level = max(10, 30 - 10*verbosity)
@@ -14,9 +15,9 @@ def configure_logging(verbosity):
 
 # The CLI command group.
 @click.group(help="Rasterio command line interface.")
- at options.verbose
- at options.quiet
- at options.version
+ at verbose_opt
+ at quiet_opt
+ at click.version_option(version=rasterio.__version__, message='%(version)s')
 @click.pass_context
 def cli(ctx, verbose, quiet):
     verbosity = verbose - quiet
@@ -43,41 +44,41 @@ def coords(obj):
                 yield f
 
 
-def write_features(file, collection,
-        agg_mode='obj', expression='feature', use_rs=False,
+def write_features(
+        fobj, collection, sequence=False, geojson_type='feature', use_rs=False,
         **dump_kwds):
     """Read an iterator of (feat, bbox) pairs and write to file using
     the selected modes."""
     # Sequence of features expressed as bbox, feature, or collection.
-    if agg_mode == 'seq':
+    if sequence:
         for feat in collection():
             xs, ys = zip(*coords(feat))
             bbox = (min(xs), min(ys), max(xs), max(ys))
             if use_rs:
-                file.write(u'\u001e')
-            if expression == 'feature':
-                file.write(json.dumps(feat, **dump_kwds))
-            elif expression == 'bbox':
-                file.write(json.dumps(bbox, **dump_kwds))
+                fobj.write(u'\u001e')
+            if geojson_type == 'feature':
+                fobj.write(json.dumps(feat, **dump_kwds))
+            elif geojson_type == 'bbox':
+                fobj.write(json.dumps(bbox, **dump_kwds))
             else:
-                file.write(
+                fobj.write(
                     json.dumps({
                         'type': 'FeatureCollection',
                         'bbox': bbox,
                         'features': [feat]}, **dump_kwds))
-            file.write('\n')
+            fobj.write('\n')
     # Aggregate all features into a single object expressed as 
     # bbox or collection.
     else:
         features = list(collection())
-        if expression == 'bbox':
-            file.write(json.dumps(collection.bbox, **dump_kwds))
-        elif expression == 'feature':
-            file.write(json.dumps(features[0], **dump_kwds))
+        if geojson_type == 'bbox':
+            fobj.write(json.dumps(collection.bbox, **dump_kwds))
+        elif geojson_type == 'feature':
+            fobj.write(json.dumps(features[0], **dump_kwds))
         else:
-            file.write(json.dumps({
+            fobj.write(json.dumps({
                 'bbox': collection.bbox,
                 'type': 'FeatureCollection', 
                 'features': features},
                 **dump_kwds))
-        file.write('\n')
+        fobj.write('\n')
diff --git a/rasterio/rio/features.py b/rasterio/rio/features.py
index ede426f..f6d4c24 100644
--- a/rasterio/rio/features.py
+++ b/rasterio/rio/features.py
@@ -5,6 +5,10 @@ import sys
 import warnings
 
 import click
+from cligj import (
+    precision_opt, indent_opt, compact_opt, projection_geographic_opt,
+    projection_projected_opt, sequence_opt, use_rs_opt,
+    geojson_type_feature_opt, geojson_type_bbox_opt)
 
 import rasterio
 from rasterio.transform import Affine
@@ -17,39 +21,15 @@ warnings.simplefilter('default')
 # Shapes command.
 @cli.command(short_help="Write the shapes of features.")
 @click.argument('input', type=click.Path(exists=True))
-# Coordinate precision option.
- at click.option('--precision', type=int, default=-1,
-              help="Decimal precision of coordinates.")
-# JSON formatting options.
- at click.option('--indent', default=None, type=int,
-              help="Indentation level for JSON output")
- at click.option('--compact/--no-compact', default=False,
-              help="Use compact separators (',', ':').")
-# Geographic (default) or Mercator switch.
- at click.option('--geographic', 'projected', flag_value='geographic',
-              default=True,
-              help="Output in geographic coordinates (the default).")
- at click.option('--projected', 'projected', flag_value='projected',
-              help="Output in projected coordinates.")
-# JSON object (default) or sequence (experimental) switch.
- at click.option('--json-obj', 'json_mode', flag_value='obj', default=True,
-        help="Write a single JSON object (the default).")
- at click.option('--x-json-seq', 'json_mode', flag_value='seq',
-        help="Write a JSON sequence. Experimental.")
-# Use ASCII RS control code to signal a sequence item (False is default).
-# See http://tools.ietf.org/html/draft-ietf-json-text-sequence-05.
-# Experimental.
- at click.option('--x-json-seq-rs/--x-json-seq-no-rs', default=False,
-        help="Use RS as text separator. Experimental.")
-# GeoJSON feature (default), bbox, or collection switch. Meaningful only
-# when --x-json-seq is used.
- at click.option('--collection', 'output_mode', flag_value='collection',
-              default=True,
-              help="Output as a GeoJSON feature collection (the default).")
- at click.option('--feature', 'output_mode', flag_value='feature',
-              help="Output as sequence of GeoJSON features.")
- at click.option('--bbox', 'output_mode', flag_value='bbox',
-              help="Output as a GeoJSON bounding box array.")
+ at precision_opt
+ at indent_opt
+ at compact_opt
+ at projection_geographic_opt
+ at projection_projected_opt
+ at sequence_opt
+ at use_rs_opt
+ at geojson_type_feature_opt(True)
+ at geojson_type_bbox_opt(False)
 @click.option('--bands/--mask', default=True,
               help="Extract shapes from one of the dataset bands or from "
                    "its nodata mask")
@@ -61,17 +41,17 @@ warnings.simplefilter('default')
               help="Include or do not include (the default) nodata regions.")
 @click.pass_context
 def shapes(
-        ctx, input, precision, indent, compact, projected, json_mode,
-        x_json_seq_rs, output_mode, bands, bidx, sampling, with_nodata):
+        ctx, input, precision, indent, compact, projection, sequence,
+        use_rs, geojson_type, bands, bidx, sampling, with_nodata):
     """Writes features of a dataset out as GeoJSON. It's intended for
     use with single-band rasters and reads from the first band.
     """
-    # These import numpy, which we don't want to do unless its needed.
+    # These import numpy, which we don't want to do unless it's needed.
     import numpy
     import rasterio.features
     import rasterio.warp
 
-    verbosity = ctx.obj['verbosity']
+    verbosity = ctx.obj['verbosity'] if ctx.obj else 1
     logger = logging.getLogger('rio')
     dump_kwds = {'sort_keys': True}
     if indent:
@@ -93,6 +73,8 @@ def shapes(
 
         def __call__(self):
             with rasterio.open(input) as src:
+                img = None
+                nodata_mask = None
                 if bands:
                     if sampling == 1:
                         img = src.read_band(bidx)
@@ -104,22 +86,22 @@ def shapes(
                             dtype=src.dtypes[src.indexes.index(bidx)])
                         img = src.read_band(bidx, img)
                         transform = src.affine * Affine.scale(float(sampling))
-                else:
+                if not bands or not with_nodata:
                     if sampling == 1:
-                        img = src.read_mask()
+                        nodata_mask = src.read_mask()
                         transform = src.transform
                     # Decimate the mask.
                     else:
-                        img = numpy.zeros(
+                        nodata_mask = numpy.zeros(
                             (src.height//sampling, src.width//sampling),
                             dtype=numpy.uint8)
-                        img = src.read_mask(img)
+                        nodata_mask = src.read_mask(nodata_mask)
                         transform = src.affine * Affine.scale(float(sampling))
 
                 bounds = src.bounds
                 xs = [bounds[0], bounds[2]]
                 ys = [bounds[1], bounds[3]]
-                if projected == 'geographic':
+                if projection == 'geographic':
                     xs, ys = rasterio.warp.transform(
                         src.crs, {'init': 'epsg:4326'}, xs, ys)
                 if precision >= 0:
@@ -129,10 +111,13 @@ def shapes(
                 self._ys = ys
 
                 kwargs = {'transform': transform}
-                if not bands and not with_nodata:
-                    kwargs['mask'] = (img==255)
+                # Default is to exclude nodata features.
+                if nodata_mask is not None:
+                    kwargs['mask'] = (nodata_mask==255)
+                if img is None:
+                    img = nodata_mask
                 for g, i in rasterio.features.shapes(img, **kwargs):
-                    if projected == 'geographic':
+                    if projection == 'geographic':
                         g = rasterio.warp.transform_geom(
                             src.crs, 'EPSG:4326', g,
                             antimeridian_cutting=True, precision=precision)
@@ -144,11 +129,14 @@ def shapes(
                         'bbox': [min(xs), min(ys), max(xs), max(ys)],
                         'geometry': g }
 
+    if not sequence:
+        geojson_type = 'collection'
+
     try:
         with rasterio.drivers(CPL_DEBUG=verbosity>2):
             write_features(
-                stdout, Collection(), agg_mode=json_mode,
-                expression=output_mode, use_rs=x_json_seq_rs,
+                stdout, Collection(), sequence=sequence,
+                geojson_type=geojson_type, use_rs=use_rs,
                 **dump_kwds)
         sys.exit(0)
     except Exception:
diff --git a/rasterio/rio/info.py b/rasterio/rio/info.py
index a3e79e0..9c34a9d 100644
--- a/rasterio/rio/info.py
+++ b/rasterio/rio/info.py
@@ -59,7 +59,8 @@ def env(ctx, key):
 @click.option('--crs', 'meta_member', flag_value='crs',
               help="Print the CRS as a PROJ.4 string.")
 @click.option('--bounds', 'meta_member', flag_value='bounds',
-              help="Print the nodata value.")
+              help="Print the boundary coordinates "
+                   "(left, bottom, right, top).")
 @click.pass_context
 def info(ctx, input, aspect, indent, namespace, meta_member):
     """Print metadata about the dataset as JSON.
diff --git a/rasterio/rio/merge.py b/rasterio/rio/merge.py
index a2a92f5..dd9335b 100644
--- a/rasterio/rio/merge.py
+++ b/rasterio/rio/merge.py
@@ -1,66 +1,142 @@
 # Merge command.
 
 import logging
+import math
 import os.path
 import sys
+import warnings
 
 import click
+from cligj import files_inout_arg, format_opt
 
 import rasterio
-
 from rasterio.rio.cli import cli
+from rasterio.transform import Affine
 
 
 @cli.command(short_help="Merge a stack of raster datasets.")
- at click.argument('input', nargs=-1,
-                type=click.Path(exists=True, resolve_path=True),
-                required=True)
- at click.option('-o','--output',
-              type=click.Path(exists=False, resolve_path=True),
-              required=True,
-              help="Path to output file.")
- at click.option('-f', '--format', '--driver', default='GTiff',
-              help="Output format driver")
+ at files_inout_arg
+ at format_opt
+ at click.option('--bounds', nargs=4, type=float, default=None,
+              help="Output bounds: left, bottom, right, top.")
+ at click.option('--res', nargs=2, type=float, default=None,
+              help="Output dataset resolution: pixel width, pixel height")
+ at click.option('--nodata', '-n', type=float, default=None,
+              help="Override nodata values defined in input datasets")
 @click.pass_context
-def merge(ctx, input, output, driver):
-    """Copy valid pixels from input files to the output file.
+def merge(ctx, files, driver, bounds, res, nodata):
+    """Copy valid pixels from input files to an output file.
+
+    All files must have the same number of bands, data type, and
+    coordinate reference system.
 
-    All files must have the same shape, number of bands, and data type.
+    Input files are merged in their listed order using the reverse
+    painter's algorithm. If the output file exists, its values will be
+    overwritten by input values.
 
-    Input files are merged in their listed order using a reverse
-    painter's algorithm.
+    Geospatial bounds and resolution of a new output file in the
+    units of the input file coordinate reference system may be provided
+    and are otherwise taken from the first input file.
     """
     import numpy as np
 
-    verbosity = ctx.obj['verbosity']
+    verbosity = (ctx.obj and ctx.obj.get('verbosity')) or 1
     logger = logging.getLogger('rio')
+
     try:
         with rasterio.drivers(CPL_DEBUG=verbosity>2):
+            output = files[-1]
+            files = files[:-1]
 
-            with rasterio.open(input[0]) as first:
+            with rasterio.open(files[0]) as first:
+                first_res = first.res
                 kwargs = first.meta
-                kwargs['transform'] = kwargs.pop('affine')
-                dest = np.empty((3,) + first.shape, dtype=first.dtypes[0])
+                kwargs.pop('affine')
+                nodataval = first.nodatavals[0]
+                dtype = first.dtypes[0]
 
             if os.path.exists(output):
+                # TODO: prompt user to update existing file (-i option) like:
+                # overwrite b.tif? (y/n [n]) n
+                # not overwritten
                 dst = rasterio.open(output, 'r+')
                 nodataval = dst.nodatavals[0]
+                dtype = dst.dtypes[0]
+                dest = np.zeros((dst.count,) + dst.shape, dtype=dtype)
             else:
+                # Create new output file.
+                # Extent from option or extent of all inputs.
+                if not bounds:
+                    # scan input files.
+                    xs = []
+                    ys = []
+                    for f in files:
+                        with rasterio.open(f) as src:
+                            left, bottom, right, top = src.bounds
+                            xs.extend([left, right])
+                            ys.extend([bottom, top])
+                    bounds = min(xs), min(ys), max(xs), max(ys)
+                output_transform = Affine.translation(bounds[0], bounds[3])
+
+                # Resolution/pixel size.
+                if not res:
+                    res = first_res
+                output_transform *= Affine.scale(res[0], -res[1])
+
+                # Dataset shape.
+                output_width = int(math.ceil((bounds[2]-bounds[0])/res[0]))
+                output_height = int(math.ceil((bounds[3]-bounds[1])/res[1]))
+
                 kwargs['driver'] == driver
-                dst = rasterio.open(output, 'w', **kwargs)
-                nodataval = first.nodatavals[0]
+                kwargs['transform'] = output_transform
+                kwargs['width'] = output_width
+                kwargs['height'] = output_height
 
-            dest.fill(nodataval)
+                dst = rasterio.open(output, 'w', **kwargs)
+                dest = np.zeros((first.count, output_height, output_width),
+                        dtype=dtype)
+
+            if nodata is not None:
+                nodataval = nodata
+
+            if nodataval is not None:
+                # Only fill if the nodataval is within dtype's range.
+                inrange = False
+                if np.dtype(dtype).kind in ('i', 'u'):
+                    info = np.iinfo(dtype)
+                    inrange = (info.min <= nodataval <= info.max)
+                elif np.dtype(dtype).kind == 'f':
+                    info = np.finfo(dtype)
+                    inrange = (info.min <= nodataval <= info.max)
+                if inrange:
+                    dest.fill(nodataval)
+                else:
+                    warnings.warn(
+                        "Input file's nodata value, %s, is beyond the valid "
+                        "range of its data type, %s. Consider overriding it "
+                        "using the --nodata option for better results." % (
+                            nodataval, dtype))
+            else:
+                nodataval = 0
 
-            for fname in reversed(input):
+            for fname in reversed(files):
                 with rasterio.open(fname) as src:
-                    data = src.read()
+                    # Real World (tm) use of boundless reads.
+                    # This approach uses the maximum amount of memory to solve
+                    # the problem. Making it more efficient is a TODO.
+                    window = src.window(*dst.bounds)
+                    data = np.zeros_like(dest)
+                    data = src.read(
+                            out=data,
+                            window=window,
+                            boundless=True,
+                            masked=True)
                     np.copyto(dest, data,
                         where=np.logical_and(
                         dest==nodataval, data.mask==False))
 
             if dst.mode == 'r+':
-                data = dst.read()
+                data = dst.read(masked=True)
                 np.copyto(dest, data,
                     where=np.logical_and(
                     dest==nodataval, data.mask==False))
@@ -72,4 +148,3 @@ def merge(ctx, input, output, driver):
     except Exception:
         logger.exception("Failed. Exception caught")
         sys.exit(1)
-
diff --git a/rasterio/rio/options.py b/rasterio/rio/options.py
deleted file mode 100644
index 7f8340f..0000000
--- a/rasterio/rio/options.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import click
-
-from rasterio import __version__ as rio_version
-
-
-def print_version(ctx, param, value):
-    if not value or ctx.resilient_parsing:
-        return
-    click.echo(rio_version)
-    ctx.exit()
-
-
-verbose = click.option('--verbose', '-v', count=True,
-                       help="Increase verbosity.")
-
-quiet = click.option('--quiet', '-q', count=True,
-                     help="Decrease verbosity.")
-
-version = click.option('--version', is_flag=True, callback=print_version,
-                       expose_value=False, is_eager=True,
-                       help="Print Rasterio version.")
diff --git a/rasterio/rio/rio.py b/rasterio/rio/rio.py
index 10df923..78b1662 100644
--- a/rasterio/rio/rio.py
+++ b/rasterio/rio/rio.py
@@ -9,9 +9,13 @@ import sys
 import warnings
 
 import click
+from cligj import (
+    precision_opt, indent_opt, compact_opt, projection_geographic_opt,
+    projection_projected_opt, projection_mercator_opt,
+    sequence_opt, use_rs_opt, geojson_type_collection_opt,
+    geojson_type_feature_opt, geojson_type_bbox_opt)
 
 import rasterio
-
 from rasterio.rio.cli import cli, write_features
 
 
@@ -27,7 +31,11 @@ warnings.simplefilter('default')
 # Insp command.
 @cli.command(short_help="Open a data file and start an interpreter.")
 @click.argument('input', type=click.Path(exists=True))
- at click.option('--mode', type=click.Choice(['r', 'r+']), default='r', help="File mode (default 'r').")
+ at click.option(
+    '--mode',
+    type=click.Choice(['r', 'r+']),
+    default='r',
+    help="File mode (default 'r').")
 @click.pass_context
 def insp(ctx, input, mode):
     import rasterio.tool
@@ -54,44 +62,20 @@ def insp(ctx, input, mode):
 # 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))
-# Coordinate precision option.
- at click.option('--precision', type=int, default=-1,
-              help="Decimal precision of coordinates.")
-# JSON formatting options.
- at click.option('--indent', default=None, type=int,
-              help="Indentation level for JSON output")
- at click.option('--compact/--no-compact', default=False,
-              help="Use compact separators (',', ':').")
-# Geographic (default) or Mercator switch.
- at click.option('--geographic', 'projected', flag_value='geographic',
-              default=True,
-              help="Output in geographic coordinates (the default).")
- at click.option('--projected', 'projected', flag_value='projected',
-              help="Output in projected coordinates.")
- at click.option('--mercator', 'projected', flag_value='mercator',
-              help="Output in Web Mercator coordinates.")
-# JSON object (default) or sequence (experimental) switch.
- at click.option('--json-obj', 'json_mode', flag_value='obj', default=True,
-        help="Write a single JSON object (the default).")
- at click.option('--x-json-seq', 'json_mode', flag_value='seq',
-        help="Write a JSON sequence. Experimental.")
-# Use ASCII RS control code to signal a sequence item (False is default).
-# See http://tools.ietf.org/html/draft-ietf-json-text-sequence-05.
-# Experimental.
- at click.option('--x-json-seq-rs/--x-json-seq-no-rs', default=False,
-        help="Use RS as text separator. Experimental.")
-# GeoJSON feature (default), bbox, or collection switch. Meaningful only
-# when --x-json-seq is used.
- at click.option('--collection', 'output_mode', flag_value='collection',
-              default=True,
-              help="Output as a GeoJSON feature collection (the default).")
- at click.option('--feature', 'output_mode', flag_value='feature',
-              help="Output as sequence of GeoJSON features.")
- at click.option('--bbox', 'output_mode', flag_value='bbox',
-              help="Output as a GeoJSON bounding box array.")
+ at precision_opt
+ at indent_opt
+ at compact_opt
+ at projection_geographic_opt
+ at projection_projected_opt
+ at projection_mercator_opt
+ at sequence_opt
+ at use_rs_opt
+ at geojson_type_collection_opt(True)
+ at geojson_type_feature_opt(False)
+ at geojson_type_bbox_opt(False)
 @click.pass_context
-def bounds(ctx, input, precision, indent, compact, projected, json_mode,
-        x_json_seq_rs, output_mode):
+def bounds(ctx, input, precision, indent, compact, projection, sequence,
+        use_rs, geojson_type):
     """Write bounding boxes to stdout as GeoJSON for use with, e.g.,
     geojsonio
 
@@ -125,10 +109,10 @@ def bounds(ctx, input, precision, indent, compact, projected, json_mode,
                     bounds = src.bounds
                     xs = [bounds[0], bounds[2]]
                     ys = [bounds[1], bounds[3]]
-                    if projected == 'geographic':
+                    if projection == 'geographic':
                         xs, ys = rasterio.warp.transform(
                             src.crs, {'init': 'epsg:4326'}, xs, ys)
-                    if projected == 'mercator':
+                    if projection == 'mercator':
                         xs, ys = rasterio.warp.transform(
                             src.crs, {'init': 'epsg:3857'}, xs, ys)
                 if precision >= 0:
@@ -153,15 +137,14 @@ def bounds(ctx, input, precision, indent, compact, projected, json_mode,
                 self._xs.extend(bbox[::2])
                 self._ys.extend(bbox[1::2])
 
-    collection = Collection()
-
+    col = Collection()
     # Use the generator defined above as input to the generic output
     # writing function.
     try:
         with rasterio.drivers(CPL_DEBUG=verbosity>2):
             write_features(
-                stdout, collection, agg_mode=json_mode,
-                expression=output_mode, use_rs=x_json_seq_rs,
+                stdout, col, sequence=sequence,
+                geojson_type=geojson_type, use_rs=use_rs,
                 **dump_kwds)
         sys.exit(0)
     except Exception:
@@ -174,8 +157,7 @@ def bounds(ctx, input, precision, indent, compact, projected, json_mode,
 @click.argument('input', default='-', required=False)
 @click.option('--src_crs', default='EPSG:4326', help="Source CRS.")
 @click.option('--dst_crs', default='EPSG:4326', help="Destination CRS.")
- at click.option('--precision', type=int, default=-1,
-              help="Decimal precision of coordinates.")
+ at precision_opt
 @click.pass_context
 def transform(ctx, input, src_crs, dst_crs, precision):
     import rasterio.warp
diff --git a/rasterio/tool.py b/rasterio/tool.py
index d0f368a..a7c2d15 100644
--- a/rasterio/tool.py
+++ b/rasterio/tool.py
@@ -18,36 +18,41 @@ logger = logging.getLogger('rasterio')
 
 Stats = collections.namedtuple('Stats', ['min', 'max', 'mean'])
 
-def main(banner, dataset):
+# Collect dictionary of functions for use in the interpreter in main()
+funcs = locals()
+
+
+def show(source, cmap='gray'):
+    """Show a raster using matplotlib.
+
+    The raster may be either an ndarray or a (dataset, bidx)
+    tuple.
+    """
+    if isinstance(source, tuple):
+        arr = source[0].read_band(source[1])
+    else:
+        arr = source
+    if plt is not None:
+        plt.imshow(arr, cmap=cmap)
+        plt.show()
+    else:
+        raise ImportError("matplotlib could not be imported")
 
-    def show(source, cmap='gray'):
-        """Show a raster using matplotlib.
-
-        The raster may be either an ndarray or a (dataset, bidx)
-        tuple.
-        """
-        if isinstance(source, tuple):
-            arr = source[0].read_band(source[1])
-        else:
-            arr = source
-        if plt is not None:
-            plt.imshow(arr, cmap=cmap)
-            plt.show()
-        else:
-            raise ImportError("matplotlib could not be imported")
-
-    def stats(source):
-        """Return a tuple with raster min, max, and mean.
-        """
-        if isinstance(source, tuple):
-            arr = source[0].read_band(source[1])
-        else:
-            arr = source
-        return Stats(numpy.min(arr), numpy.max(arr), numpy.mean(arr))
 
+def stats(source):
+    """Return a tuple with raster min, max, and mean.
+    """
+    if isinstance(source, tuple):
+        arr = source[0].read_band(source[1])
+    else:
+        arr = source
+    return Stats(numpy.min(arr), numpy.max(arr), numpy.mean(arr))
+
+
+def main(banner, dataset):
+    """ Main entry point for use with interpreter """
     code.interact(
-        banner, 
-        local=dict(
-            locals(), src=dataset, np=numpy, rio=rasterio, plt=plt))
+        banner,
+        local=dict(funcs, src=dataset, np=numpy, rio=rasterio, plt=plt))
 
     return 0
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 41a8d9f..fb826ea 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,7 +1,10 @@
-git+https://github.com/sgillies/affine.git#egg=affine
-Cython>=0.20
-Numpy>=1.8.0
-pytest
+affine
+cligj
 coveralls>=0.4
+cython>=0.20
+delocate
+enum34
+numpy>=1.8.0
+pytest
 setuptools
-six
+wheel
diff --git a/requirements.txt b/requirements.txt
index e095453..66a3935 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
-affine>=1.0
-click
+affine
+cligj
 enum34
-numpy>=1.8
+numpy>=1.8.0
 setuptools
diff --git a/setup.py b/setup.py
index 7e39bfa..cd491cf 100755
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,17 @@
 #!/usr/bin/env python
+
+# Two environmental variables influence this script.
+#
+# GDAL_CONFIG: the path to a gdal-config program that points to GDAL headers,
+# libraries, and data files.
+#
+# PACKAGE_DATA: if defined, GDAL and PROJ4 data files will be copied into the
+# source or binary distribution. This is essential when creating self-contained
+# binary wheels.
+
 import logging
 import os
+import shutil
 import subprocess
 import sys
 from setuptools import setup
@@ -43,13 +54,15 @@ except ImportError:
     sys.exit(1)
 
 try:
-    gdal_config = "gdal-config"
+    gdal_config = os.environ.get('GDAL_CONFIG', 'gdal-config')
     with open("gdal-config.txt", "w") as gcfg:
         subprocess.call([gdal_config, "--cflags"], stdout=gcfg)
         subprocess.call([gdal_config, "--libs"], stdout=gcfg)
+        subprocess.call([gdal_config, "--datadir"], stdout=gcfg)
     with open("gdal-config.txt", "r") as gcfg:
         cflags = gcfg.readline().strip()
         libs = gcfg.readline().strip()
+        datadir = gcfg.readline().strip()
     for item in cflags.split():
         if item.startswith("-I"):
             include_dirs.extend(item[2:].split(":"))
@@ -61,9 +74,30 @@ try:
         else:
             # e.g. -framework GDAL
             extra_link_args.append(item)
+
+    # Conditionally copy the GDAL data. To be used in conjunction with
+    # the bdist_wheel command to make self-contained binary wheels.
+    if os.environ.get('PACKAGE_DATA'):
+        try:
+            shutil.rmtree('rasterio/gdal_data')
+        except OSError:
+            pass
+        shutil.copytree(datadir, 'rasterio/gdal_data')
+
 except Exception as e:
     log.warning("Failed to get options via gdal-config: %s", str(e))
 
+# Conditionally copy PROJ.4 data. Presumes PROJ.4 is installed locally
+# with --prefix=/usr/local.
+if os.environ.get('PACKAGE_DATA'):
+    projdatadir = '/usr/local/share/proj'
+    if os.path.exists(projdatadir):
+        try:
+            shutil.rmtree('rasterio/proj_data')
+        except OSError:
+            pass
+        shutil.copytree(projdatadir, 'rasterio/proj_data')
+
 ext_options = dict(
     include_dirs=include_dirs,
     library_dirs=library_dirs,
@@ -124,43 +158,47 @@ with open('README.rst') as f:
 # Runtime requirements.
 inst_reqs = [
     'affine>=1.0',
-    'click>=3.0',
+    'cligj',
     'Numpy>=1.7' ]
 
 if sys.version_info < (3, 4):
     inst_reqs.append('enum34')
 
-setup(name='rasterio',
-      version=version,
-      description=(
-          "Fast and direct raster I/O for Python programmers who use Numpy"),
-      long_description=readme,
-      classifiers=[
-          'Development Status :: 4 - Beta',
-          'Intended Audience :: Developers',
-          'Intended Audience :: Information Technology',
-          'Intended Audience :: Science/Research',
-          'License :: OSI Approved :: BSD License',
-          'Programming Language :: C',
-          'Programming Language :: Python :: 2.6',
-          'Programming Language :: Python :: 2.7',
-          'Programming Language :: Python :: 3.3',
-          'Programming Language :: Python :: 3.4',
-          'Topic :: Multimedia :: Graphics :: Graphics Conversion',
-          'Topic :: Scientific/Engineering :: GIS'
-          ],
-      keywords='raster gdal',
-      author='Sean Gillies',
-      author_email='sean at mapbox.com',
-      url='https://github.com/mapbox/rasterio',
-      license='BSD',
-      package_dir={'': '.'},
-      packages=['rasterio', 'rasterio.rio'],
-      entry_points='''
+setup_args = dict(
+    name='rasterio',
+    version=version,
+    description="Fast and direct raster I/O for use with Numpy and SciPy",
+    long_description=readme,
+    classifiers=[
+        'Development Status :: 4 - Beta',
+        'Intended Audience :: Developers',
+        'Intended Audience :: Information Technology',
+        'Intended Audience :: Science/Research',
+        'License :: OSI Approved :: BSD License',
+        'Programming Language :: C',
+        'Programming Language :: Python :: 2.6',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3.3',
+        'Programming Language :: Python :: 3.4',
+        'Topic :: Multimedia :: Graphics :: Graphics Conversion',
+        'Topic :: Scientific/Engineering :: GIS'],
+    keywords='raster gdal',
+    author='Sean Gillies',
+    author_email='sean at mapbox.com',
+    url='https://github.com/mapbox/rasterio',
+    license='BSD',
+    package_dir={'': '.'},
+    packages=['rasterio', 'rasterio.rio'],
+    entry_points='''
         [console_scripts]
         rio=rasterio.rio.main:cli
-      ''',
-      include_package_data=True,
-      ext_modules=ext_modules,
-      zip_safe=False,
-      install_requires=inst_reqs )
+    ''',
+    include_package_data=True,
+    ext_modules=ext_modules,
+    zip_safe=False,
+    install_requires=inst_reqs)
+
+if os.environ.get('PACKAGE_DATA'):
+    setup_args['package_data'] = {'rasterio': ['gdal_data/*', 'proj_data/*']}
+
+setup(**setup_args)
diff --git a/tests/test_cli.py b/tests/test_cli.py
deleted file mode 100644
index 35cbabb..0000000
--- a/tests/test_cli.py
+++ /dev/null
@@ -1,115 +0,0 @@
-import subprocess
-
-
-def test_cli_bounds_obj_bbox():
-    result = subprocess.check_output(
-        'rio bounds tests/data/RGB.byte.tif --bbox --precision 6',
-        shell=True)
-    assert result.decode('utf-8').strip() == '[-78.898133, 23.564991, -76.599438, 25.550874]'
-
-
-def test_cli_bounds_obj_bbox_mercator():
-    result = subprocess.check_output(
-        'rio bounds tests/data/RGB.byte.tif --bbox --mercator --precision 3',
-        shell=True)
-    assert result.decode('utf-8').strip() == '[-8782900.033, 2700489.278, -8527010.472, 2943560.235]'
-
-
-def test_cli_bounds_obj_feature():
-    result = subprocess.check_output(
-        'rio bounds tests/data/RGB.byte.tif --feature --precision 6',
-        shell=True)
-    assert result.decode('utf-8').strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "0", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}'
-
-
-def test_cli_bounds_obj_collection():
-    result = subprocess.check_output(
-        'rio bounds tests/data/RGB.byte.tif --precision 6',
-        shell=True)
-    assert result.decode('utf-8').strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "features": [{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "0", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}], "type": "FeatureCollection"}'
-
-
-def test_cli_bounds_seq_feature_rs():
-    result = subprocess.check_output(
-        'rio bounds tests/data/RGB.byte.tif --x-json-seq --x-json-seq-rs --feature --precision 6',
-        shell=True)
-    assert result.decode('utf-8').startswith(u'\x1e')
-    assert result.decode('utf-8').strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "0", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}'
-
-
-def test_cli_bounds_seq_collection():
-    result = subprocess.check_output(
-        'rio bounds tests/data/RGB.byte.tif --x-json-seq --x-json-seq-rs --precision 6',
-        shell=True)
-    assert result.decode('utf-8').startswith(u'\x1e')
-    assert result.decode('utf-8').strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "features": [{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "0", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}], "type": "FeatureCollection"}'
-
-
-def test_cli_bounds_seq_bbox():
-    result = subprocess.check_output(
-        'rio bounds tests/data/RGB.byte.tif --x-json-seq --x-json-seq-rs --bbox --precision 6',
-        shell=True)
-    assert result.decode('utf-8').startswith(u'\x1e')
-    assert result.decode('utf-8').strip() == '[-78.898133, 23.564991, -76.599438, 25.550874]'
-
-
-def test_cli_bounds_seq_collection_multi(tmpdir):
-    filename = str(tmpdir.join("test.json"))
-    tmp = open(filename, 'w')
-
-    subprocess.check_call(
-        'rio bounds tests/data/RGB.byte.tif tests/data/RGB.byte.tif --x-json-seq --x-json-seq-rs --precision 6',
-        stdout=tmp,
-        shell=True)
-
-    tmp.close()
-    tmp = open(filename, 'r')
-    json_texts = []
-    text = ""
-    for line in tmp:
-        rs_idx = line.find(u'\x1e')
-        if rs_idx >= 0:
-            if text:
-                text += line[:rs_idx]
-                json_texts.append(text)
-            text = line[rs_idx+1:]
-        else:
-            text += line
-    else:
-        json_texts.append(text)
-
-    assert len(json_texts) == 2
-    assert json_texts[0].strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "features": [{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "0", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}], "type": "FeatureCollection"}'
-    assert json_texts[1].strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "features": [{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "1", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}], "type": "FeatureCollection"}'
-
-
-def test_cli_info_count():
-    result = subprocess.check_output(
-        'if [ `rio info tests/data/RGB.byte.tif --count` -eq 3 ]; '
-        'then echo "True"; fi',
-        shell=True)
-    assert result.decode('utf-8').strip() == 'True'
-
-
-def test_cli_info_nodata():
-    result = subprocess.check_output(
-        'if [ `rio info tests/data/RGB.byte.tif --nodata` = "0.0" ]; '
-        'then echo "True"; fi',
-        shell=True)
-    assert result.decode('utf-8').strip() == 'True'
-
-
-def test_cli_info_dtype():
-    result = subprocess.check_output(
-        'if [ `rio info tests/data/RGB.byte.tif --dtype` = "uint8" ]; '
-        'then echo "True"; fi',
-        shell=True)
-    assert result.decode('utf-8').strip() == 'True'
-
-
-def test_cli_info_shape():
-    result = subprocess.check_output(
-        'if [[ `rio info tests/data/RGB.byte.tif --shape` == "718 791" ]]; '
-        'then echo "True"; fi',
-        shell=True, executable='/bin/bash')
-    assert result.decode('utf-8').strip() == 'True'
diff --git a/tests/test_indexing.py b/tests/test_indexing.py
index fbc57b4..317ddb3 100644
--- a/tests/test_indexing.py
+++ b/tests/test_indexing.py
@@ -13,13 +13,9 @@ def test_full_window():
     with rasterio.open('tests/data/RGB.byte.tif') as src:
         assert src.window(*src.bounds) == tuple(zip((0, 0), src.shape))
 
-def test_window_exception():
+def test_window_no_exception():
     with rasterio.open('tests/data/RGB.byte.tif') as src:
         left, bottom, right, top = src.bounds
         left -= 1000.0
-        try:
-            _ = src.window(left, bottom, right, top)
-            assert False
-        except ValueError:
-            assert True
-
+        assert src.window(left, bottom, right, top) == (
+                (0, src.height), (-3, src.width))
diff --git a/tests/test_read.py b/tests/test_read.py
index 61bd736..98cf22d 100644
--- a/tests/test_read.py
+++ b/tests/test_read.py
@@ -90,19 +90,6 @@ class ReaderContextTest(unittest.TestCase):
             except:
                 assert "failed to catch exception" is False
 
-    def test_read_out_shape_resample(self):
-        with rasterio.open('tests/data/RGB.byte.tif') as s:
-            a = numpy.zeros((7, 8), dtype=rasterio.ubyte)
-            s.read_band(1, a)
-            self.assert_(
-                repr(a) == """array([[  0,   8,   5,   7,   0,   0,   0,   0],
-       [  0,   6,  61,  15,  27,  15,  24, 128],
-       [  0,  20, 152,  23,  15,  19,  28,   0],
-       [  0,  17, 255,  25, 255,  22,  32,   0],
-       [  9,   7,  14,  16,  19,  18,  36,   0],
-       [  6,  27,  43, 207,  38,  31,  73,   0],
-       [  0,   0,   0,   0,  74,  23,   0,   0]], dtype=uint8)""", a)
-
     def test_read_basic(self):
         with rasterio.open('tests/data/shade.tif') as s:
             a = s.read()  # Gray
diff --git a/tests/test_read_boundless.py b/tests/test_read_boundless.py
new file mode 100644
index 0000000..e6c2a1f
--- /dev/null
+++ b/tests/test_read_boundless.py
@@ -0,0 +1,51 @@
+import logging
+import sys
+
+import numpy
+
+import rasterio
+
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+
+def test_read_boundless_natural_extent():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        data = src.read(boundless=True)
+        assert data.shape == (3, src.height, src.width)
+        assert abs(data[0].mean() - src.read(1).mean()) < 0.0001
+        assert data.any()
+
+def test_read_boundless_beyond():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        data = src.read(window=((-200, -100), (-200, -100)), boundless=True)
+        assert data.shape == (3, 100, 100)
+        assert not data.any()
+
+
+def test_read_boundless_beyond2():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        data = src.read(window=((1000, 1100), (1000, 1100)), boundless=True)
+        assert data.shape == (3, 100, 100)
+        assert not data.any()
+
+
+def test_read_boundless_overlap():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        data = src.read(window=((-200, 200), (-200, 200)), boundless=True)
+        assert data.shape == (3, 400, 400)
+        assert data.any()
+        assert data[0,399,399] == 13
+
+
+def test_read_boundless_resample():
+    with rasterio.open('tests/data/RGB.byte.tif') as src:
+        out = numpy.zeros((3, 800, 800), dtype=numpy.uint8)
+        data = src.read(
+                out=out,
+                window=((-200, 200), (-200, 200)),
+                masked=True,
+                boundless=True)
+        assert data.shape == (3, 800, 800)
+        assert data.any()
+        assert data[0,798,798] == 13
diff --git a/tests/test_read_resample.py b/tests/test_read_resample.py
new file mode 100644
index 0000000..f3eb1bc
--- /dev/null
+++ b/tests/test_read_resample.py
@@ -0,0 +1,33 @@
+import numpy
+
+import rasterio
+
+
+# Rasterio exposes GDAL's resampling/decimation on I/O. These are the tests
+# that it does this correctly.
+#
+# Rasterio's test dataset is 718 rows by 791 columns.
+
+def test_read_out_shape_resample_down():
+    with rasterio.open('tests/data/RGB.byte.tif') as s:
+        out = numpy.zeros((7, 8), dtype=rasterio.ubyte)
+        data = s.read(1, out=out)
+        expected = numpy.array([
+            [  0,   8,   5,   7,   0,   0,   0,   0],
+            [  0,   6,  61,  15,  27,  15,  24, 128],
+            [  0,  20, 152,  23,  15,  19,  28,   0],
+            [  0,  17, 255,  25, 255,  22,  32,   0],
+            [  9,   7,  14,  16,  19,  18,  36,   0],
+            [  6,  27,  43, 207,  38,  31,  73,   0],
+            [  0,   0,   0,   0,  74,  23,   0,   0]], dtype=numpy.uint8)
+        assert (data == expected).all() # all True.
+
+
+def test_read_out_shape_resample_up():
+    # Instead of testing array items, test statistics. Upsampling by an even
+    # constant factor shouldn't change the mean.
+    with rasterio.open('tests/data/RGB.byte.tif') as s:
+        out = numpy.zeros((7180, 7910), dtype=rasterio.ubyte)
+        data = s.read(1, out=out, masked=True)
+        assert data.shape == (7180, 7910)
+        assert data.mean() == s.read(1).mean()
diff --git a/tests/test_rio_bands.py b/tests/test_rio_bands.py
index b25b293..dfc6b18 100644
--- a/tests/test_rio_bands.py
+++ b/tests/test_rio_bands.py
@@ -14,7 +14,7 @@ def test_stack(tmpdir):
     runner = CliRunner()
     result = runner.invoke(
         bands.stack,
-        ['tests/data/RGB.byte.tif', '-o', outputname],
+        ['tests/data/RGB.byte.tif', outputname],
         catch_exceptions=False)
     assert result.exit_code == 0
     with rasterio.open(outputname) as out:
@@ -26,7 +26,7 @@ def test_stack_list(tmpdir):
     runner = CliRunner()
     result = runner.invoke(
         bands.stack,
-        ['tests/data/RGB.byte.tif', '--bidx', '1,2,3', '-o', outputname])
+        ['tests/data/RGB.byte.tif', '--bidx', '1,2,3', outputname])
     assert result.exit_code == 0
     with rasterio.open(outputname) as out:
         assert out.count == 3
@@ -40,7 +40,7 @@ def test_stack_slice(tmpdir):
         [
             'tests/data/RGB.byte.tif', '--bidx', '..2',
             'tests/data/RGB.byte.tif', '--bidx', '3..',
-            '-o', outputname])
+            outputname])
     assert result.exit_code == 0
     with rasterio.open(outputname) as out:
         assert out.count == 3
@@ -55,7 +55,7 @@ def test_stack_single_slice(tmpdir):
             'tests/data/RGB.byte.tif', '--bidx', '1',
             'tests/data/RGB.byte.tif', '--bidx', '2..',
             '--photometric', 'rgb',
-            '-o', outputname])
+            outputname])
     assert result.exit_code == 0
     with rasterio.open(outputname) as out:
         assert out.count == 3
@@ -66,7 +66,7 @@ def test_format_jpeg(tmpdir):
     runner = CliRunner()
     result = runner.invoke(
         bands.stack,
-        ['tests/data/RGB.byte.tif', '-o', outputname, '--format', 'JPEG'])
+        ['tests/data/RGB.byte.tif', outputname, '--format', 'JPEG'])
     assert result.exit_code == 0
 
 
@@ -75,5 +75,5 @@ def test_error(tmpdir):
     runner = CliRunner()
     result = runner.invoke(
         bands.stack,
-        ['tests/data/RGB.byte.tif', '-o', outputname, '--driver', 'BOGUS'])
+        ['tests/data/RGB.byte.tif', outputname, '--driver', 'BOGUS'])
     assert result.exit_code == 1
diff --git a/tests/test_rio_features.py b/tests/test_rio_features.py
new file mode 100644
index 0000000..c886515
--- /dev/null
+++ b/tests/test_rio_features.py
@@ -0,0 +1,99 @@
+import logging
+import re
+import sys
+
+import click
+from click.testing import CliRunner
+
+from rasterio.rio import features
+
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+
+def test_err():
+    runner = CliRunner()
+    result = runner.invoke(
+        features.shapes, ['tests/data/shade.tif', '--bidx', '4'])
+    assert result.exit_code == 1
+
+
+def test_shapes():
+    runner = CliRunner()
+    result = runner.invoke(features.shapes, ['tests/data/shade.tif'])
+    assert result.exit_code == 0
+    assert result.output.count('"FeatureCollection"') == 1
+    assert result.output.count('"Feature"') == 232
+
+
+def test_shapes_sequence():
+    runner = CliRunner()
+    result = runner.invoke(features.shapes, ['tests/data/shade.tif', '--sequence'])
+    assert result.exit_code == 0
+    assert result.output.count('"FeatureCollection"') == 0
+    assert result.output.count('"Feature"') == 232
+
+
+def test_shapes_sequence_rs():
+    runner = CliRunner()
+    result = runner.invoke(
+        features.shapes, [
+            'tests/data/shade.tif',
+            '--sequence',
+            '--rs'])
+    assert result.exit_code == 0
+    assert result.output.count('"FeatureCollection"') == 0
+    assert result.output.count('"Feature"') == 232
+    assert result.output.count(u'\u001e') == 232
+
+
+def test_shapes_with_nodata():
+    runner = CliRunner()
+    result = runner.invoke(features.shapes, ['tests/data/shade.tif', '--with-nodata'])
+    assert result.exit_code == 0
+    assert result.output.count('"FeatureCollection"') == 1
+    assert result.output.count('"Feature"') == 288
+
+
+def test_shapes_indent():
+    runner = CliRunner()
+    result = runner.invoke(features.shapes, ['tests/data/shade.tif', '--indent', '2'])
+    assert result.exit_code == 0
+    assert result.output.count('"FeatureCollection"') == 1
+    assert result.output.count('\n') == 70139
+
+
+def test_shapes_compact():
+    runner = CliRunner()
+    result = runner.invoke(features.shapes, ['tests/data/shade.tif', '--compact'])
+    assert result.exit_code == 0
+    assert result.output.count('"FeatureCollection"') == 1
+    assert result.output.count(', ') == 0
+    assert result.output.count(': ') == 0
+
+
+def test_shapes_sampling():
+    runner = CliRunner()
+    result = runner.invoke(
+        features.shapes, ['tests/data/shade.tif', '--sampling', '10'])
+    assert result.exit_code == 0
+    assert result.output.count('"FeatureCollection"') == 1
+    assert result.output.count('"Feature"') == 124
+
+
+def test_shapes_precision():
+    runner = CliRunner()
+    result = runner.invoke(
+        features.shapes, ['tests/data/shade.tif', '--precision', '1'])
+    assert result.exit_code == 0
+    assert result.output.count('"FeatureCollection"') == 1
+    # Find no numbers with 2+ decimal places.
+    assert re.search(r'\d*\.\d{2,}', result.output) is None
+
+
+def test_shapes_mask():
+    runner = CliRunner()
+    result = runner.invoke(features.shapes, ['tests/data/RGB.byte.tif', '--mask'])
+    assert result.exit_code == 0
+    assert result.output.count('"FeatureCollection"') == 1
+    assert result.output.count('"Feature"') == 9
diff --git a/tests/test_rio_info.py b/tests/test_rio_info.py
index fd56e20..fce04eb 100644
--- a/tests/test_rio_info.py
+++ b/tests/test_rio_info.py
@@ -3,7 +3,7 @@ from click.testing import CliRunner
 
 
 import rasterio
-from rasterio.rio import info
+from rasterio.rio import cli, info
 
 
 def test_env():
@@ -30,6 +30,22 @@ def test_info():
     assert '"count": 3' in result.output
 
 
+def test_info_verbose():
+    runner = CliRunner()
+    result = runner.invoke(
+        cli.cli,
+        ['-v', 'info', 'tests/data/RGB.byte.tif'])
+    assert result.exit_code == 0
+
+
+def test_info_quiet():
+    runner = CliRunner()
+    result = runner.invoke(
+        cli.cli,
+        ['-q', 'info', 'tests/data/RGB.byte.tif'])
+    assert result.exit_code == 0
+
+
 def test_info_count():
     runner = CliRunner()
     result = runner.invoke(
diff --git a/tests/test_rio_merge.py b/tests/test_rio_merge.py
new file mode 100644
index 0000000..ddcb0a2
--- /dev/null
+++ b/tests/test_rio_merge.py
@@ -0,0 +1,247 @@
+import sys
+import os
+import logging
+import click
+import numpy
+from click.testing import CliRunner
+from pytest import fixture
+
+import rasterio
+from rasterio.rio.merge import merge
+
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+
+# Fixture to create test datasets within temporary directory
+ at fixture(scope='function')
+def test_data_dir_1(tmpdir):
+    kwargs = {
+        "crs": {'init': 'epsg:4326'},
+        "transform": (-114, 0.2, 0, 46, 0, -0.2),
+        "count": 1,
+        "dtype": rasterio.uint8,
+        "driver": "GTiff",
+        "width": 10,
+        "height": 10,
+        "nodata": 1
+    }
+
+    with rasterio.drivers():
+
+        with rasterio.open(str(tmpdir.join('a.tif')), 'w', **kwargs) as dst:
+            data = numpy.ones((10, 10), dtype=rasterio.uint8)
+            data[0:6, 0:6] = 255
+            dst.write_band(1, data)
+
+        with rasterio.open(str(tmpdir.join('b.tif')), 'w', **kwargs) as dst:
+            data = numpy.ones((10, 10), dtype=rasterio.uint8)
+            data[4:8, 4:8] = 254
+            dst.write_band(1, data)
+
+    return tmpdir
+
+
+ at fixture(scope='function')
+def test_data_dir_2(tmpdir):
+    kwargs = {
+        "crs": {'init': 'epsg:4326'},
+        "transform": (-114, 0.2, 0, 46, 0, -0.1),
+        "count": 1,
+        "dtype": rasterio.uint8,
+        "driver": "GTiff",
+        "width": 10,
+        "height": 10
+        # these files have undefined nodata.
+    }
+
+    with rasterio.drivers():
+
+        with rasterio.open(str(tmpdir.join('a.tif')), 'w', **kwargs) as dst:
+            data = numpy.zeros((10, 10), dtype=rasterio.uint8)
+            data[0:6, 0:6] = 255
+            dst.write_band(1, data)
+
+        with rasterio.open(str(tmpdir.join('b.tif')), 'w', **kwargs) as dst:
+            data = numpy.zeros((10, 10), dtype=rasterio.uint8)
+            data[4:8, 4:8] = 254
+            dst.write_band(1, data)
+
+    return tmpdir
+
+
+def test_merge_with_nodata(test_data_dir_1):
+    outputname = str(test_data_dir_1.join('merged.tif'))
+    inputs = [str(x) for x in test_data_dir_1.listdir()]
+    inputs.sort()
+    runner = CliRunner()
+    result = runner.invoke(merge, inputs + [outputname])
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+    with rasterio.open(outputname) as out:
+        assert out.count == 1
+        data = out.read_band(1, masked=False)
+        expected = numpy.ones((10, 10), dtype=rasterio.uint8)
+        expected[0:6, 0:6] = 255
+        expected[4:8, 4:8] = 254
+        assert numpy.all(data == expected)
+
+
+def test_merge_warn(test_data_dir_1):
+    outputname = str(test_data_dir_1.join('merged.tif'))
+    inputs = [str(x) for x in test_data_dir_1.listdir()]
+    inputs.sort()
+    runner = CliRunner()
+    result = runner.invoke(merge, inputs + [outputname] + ['-n', '-1'])
+    assert result.exit_code == 0
+    assert "using the --nodata option for better results" in result.output
+
+
+def test_merge_without_nodata(test_data_dir_2):
+    outputname = str(test_data_dir_2.join('merged.tif'))
+    inputs = [str(x) for x in test_data_dir_2.listdir()]
+    inputs.sort()
+    runner = CliRunner()
+    result = runner.invoke(merge, inputs + [outputname])
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+    with rasterio.open(outputname) as out:
+        assert out.count == 1
+        data = out.read_band(1, masked=False)
+        expected = numpy.zeros((10, 10), dtype=rasterio.uint8)
+        expected[0:6, 0:6] = 255
+        expected[4:8, 4:8] = 254
+        assert numpy.all(data == expected)
+
+
+def test_merge_output_exists(tmpdir):
+    outputname = str(tmpdir.join('merged.tif'))
+    runner = CliRunner()
+    result = runner.invoke(
+        merge,
+        ['tests/data/RGB.byte.tif', outputname])
+    assert result.exit_code == 0
+    result = runner.invoke(
+        merge,
+        ['tests/data/RGB.byte.tif', outputname])
+    assert os.path.exists(outputname)
+    with rasterio.open(outputname) as out:
+        assert out.count == 3
+
+
+def test_merge_output_exists_without_nodata(test_data_dir_2):
+    runner = CliRunner()
+    result = runner.invoke(
+        merge,
+        [str(test_data_dir_2.join('a.tif')),
+            str(test_data_dir_2.join('b.tif'))])
+    assert result.exit_code == 0
+
+
+def test_merge_err():
+    runner = CliRunner()
+    result = runner.invoke(
+        merge,
+        ['tests'])
+    assert result.exit_code == 1
+
+
+def test_format_jpeg(tmpdir):
+    outputname = str(tmpdir.join('stacked.jpg'))
+    runner = CliRunner()
+    result = runner.invoke(
+        merge,
+        ['tests/data/RGB.byte.tif', outputname, '--format', 'JPEG'])
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+
+
+# Non-coincident datasets test fixture.
+# Two overlapping GeoTIFFs, one to the NW and one to the SE.
+ at fixture(scope='function')
+def test_data_dir_overlapping(tmpdir):
+    kwargs = {
+        "crs": {'init': 'epsg:4326'},
+        "transform": (-114, 0.2, 0, 46, 0, -0.2),
+        "count": 1,
+        "dtype": rasterio.uint8,
+        "driver": "GTiff",
+        "width": 10,
+        "height": 10,
+        "nodata": 0
+    }
+
+    with rasterio.drivers():
+        with rasterio.open(str(tmpdir.join('nw.tif')), 'w', **kwargs) as dst:
+            data = numpy.ones((10, 10), dtype=rasterio.uint8)
+            dst.write_band(1, data)
+
+        kwargs['transform'] = (-113, 0.2, 0, 45, 0, -0.2)
+        with rasterio.open(str(tmpdir.join('se.tif')), 'w', **kwargs) as dst:
+            data = numpy.ones((10, 10), dtype=rasterio.uint8) * 2
+            dst.write_band(1, data)
+
+    return tmpdir
+
+
+def test_merge_overlapping(test_data_dir_overlapping):
+    outputname = str(test_data_dir_overlapping.join('merged.tif'))
+    inputs = [str(x) for x in test_data_dir_overlapping.listdir()]
+    inputs.sort()
+    runner = CliRunner()
+    result = runner.invoke(merge, inputs + [outputname])
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+    with rasterio.open(outputname) as out:
+        assert out.count == 1
+        assert out.shape == (15, 15)
+        assert out.bounds == (-114, 43, -111, 46)
+        data = out.read_band(1, masked=False)
+        expected = numpy.zeros((15, 15), dtype=rasterio.uint8)
+        expected[0:10, 0:10] = 1
+        expected[5:, 5:] = 2
+        assert numpy.all(data == expected)
+
+
+# Fixture to create test datasets within temporary directory
+ at fixture(scope='function')
+def test_data_dir_float(tmpdir):
+    kwargs = {
+        "crs": {'init': 'epsg:4326'},
+        "transform": (-114, 0.2, 0, 46, 0, -0.2),
+        "count": 1,
+        "dtype": rasterio.float64,
+        "driver": "GTiff",
+        "width": 10,
+        "height": 10,
+        "nodata": 0
+    }
+
+    with rasterio.drivers():
+        with rasterio.open(str(tmpdir.join('one.tif')), 'w', **kwargs) as dst:
+            data = numpy.zeros((10, 10), dtype=rasterio.float64)
+            data[0:6, 0:6] = 255
+            dst.write_band(1, data)
+
+        with rasterio.open(str(tmpdir.join('two.tif')), 'w', **kwargs) as dst:
+            data = numpy.zeros((10, 10), dtype=rasterio.float64)
+            data[4:8, 4:8] = 254
+            dst.write_band(1, data)
+    return tmpdir
+
+
+def test_merge_float(test_data_dir_float):
+    outputname = str(test_data_dir_float.join('merged.tif'))
+    inputs = [str(x) for x in test_data_dir_float.listdir()]
+    inputs.sort()
+    runner = CliRunner()
+    result = runner.invoke(merge, inputs + [outputname] + ['-n', '-1.5'])
+    assert result.exit_code == 0
+    assert os.path.exists(outputname)
+    with rasterio.open(outputname) as out:
+        assert out.count == 1
+        data = out.read_band(1, masked=False)
+        expected = numpy.ones((10, 10), dtype=rasterio.float64) * -1.5
+        expected[0:6, 0:6] = 255
+        expected[4:8, 4:8] = 254
+        assert numpy.all(data == expected)
diff --git a/tests/test_rio_options.py b/tests/test_rio_options.py
deleted file mode 100644
index a4b3e31..0000000
--- a/tests/test_rio_options.py
+++ /dev/null
@@ -1,15 +0,0 @@
-import click
-from click.testing import CliRunner
-
-
-import rasterio
-from rasterio.rio import rio
-
-
-def test_insp():
-    runner = CliRunner()
-    result = runner.invoke(
-        rio.cli,
-        ['--version'])
-    assert result.exit_code == 0
-    assert result.output.strip() == rasterio.__version__
diff --git a/tests/test_rio_rio.py b/tests/test_rio_rio.py
index 4780e33..06a7774 100644
--- a/tests/test_rio_rio.py
+++ b/tests/test_rio_rio.py
@@ -3,7 +3,14 @@ from click.testing import CliRunner
 
 
 import rasterio
-from rasterio.rio import rio
+from rasterio.rio import cli, rio
+
+
+def test_version():
+    runner = CliRunner()
+    result = runner.invoke(cli.cli, ['--version'])
+    assert result.exit_code == 0
+    assert rasterio.__version__ in result.output
 
 
 def test_insp():
@@ -39,6 +46,15 @@ def test_bounds_err():
     assert result.exit_code == 1
 
 
+def test_bounds_feature():
+    runner = CliRunner()
+    result = runner.invoke(
+        rio.bounds,
+        ['tests/data/RGB.byte.tif', '--feature'])
+    assert result.exit_code == 0
+    assert result.output.count('Polygon') == 1
+
+
 def test_bounds_obj_bbox():
     runner = CliRunner()
     result = runner.invoke(
@@ -88,29 +104,25 @@ def test_bounds_seq():
     runner = CliRunner()
     result = runner.invoke(
         rio.bounds,
-        ['tests/data/RGB.byte.tif', 'tests/data/RGB.byte.tif', '--x-json-seq', '--bbox', '--precision', '2'])
+        ['tests/data/RGB.byte.tif', 'tests/data/RGB.byte.tif', '--sequence'])
     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'
-    assert '\x1e' not in result.output
-
+    assert result.output.count('Polygon') == 2
 
-def test_bounds_seq_rs():
-    runner = CliRunner()
     result = runner.invoke(
         rio.bounds,
-        ['tests/data/RGB.byte.tif', 'tests/data/RGB.byte.tif', '--x-json-seq', '--x-json-seq-rs', '--bbox', '--precision', '2'])
+        ['tests/data/RGB.byte.tif', 'tests/data/RGB.byte.tif', '--sequence', '--bbox', '--precision', '2'])
     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'
-
+    assert result.output == '[-78.9, 23.56, -76.6, 25.55]\n[-78.9, 23.56, -76.6, 25.55]\n'
+    assert '\x1e' not in result.output
 
 
-def test_bounds_obj_feature():
+def test_bounds_seq_rs():
     runner = CliRunner()
     result = runner.invoke(
         rio.bounds,
-        ['tests/data/RGB.byte.tif', '--feature', '--precision', '6'])
+        ['tests/data/RGB.byte.tif', 'tests/data/RGB.byte.tif', '--sequence', '--rs', '--bbox', '--precision', '2'])
     assert result.exit_code == 0
-    assert result.output.strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "0", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}'
+    assert result.output == '\x1e[-78.9, 23.56, -76.6, 25.55]\n\x1e[-78.9, 23.56, -76.6, 25.55]\n'
 
 
 def test_transform_err():
diff --git a/tests/test_tool.py b/tests/test_tool.py
new file mode 100644
index 0000000..5fa4b03
--- /dev/null
+++ b/tests/test_tool.py
@@ -0,0 +1,44 @@
+import numpy as np
+
+try:
+    import matplotlib.pyplot as plt
+except ImportError:
+    plt = None
+
+import rasterio
+from rasterio.tool import show, stats
+
+
+def test_stats():
+    with rasterio.drivers():
+        with rasterio.open('tests/data/RGB.byte.tif') as src:
+            results = stats((src, 1))
+            assert results[0] == 1
+            assert results[1] == 255
+            assert np.isclose(results[2], 44.4344)
+
+            results2 = stats(src.read_band(1))
+            assert np.allclose(np.array(results), np.array(results2))
+
+
+def test_show():
+    """
+    This test only verifies that code up to the point of plotting with
+    matplotlib works correctly.  Tests do not exercise matplotlib.
+    """
+    if plt:
+        # Return because plotting causes the tests to block until the plot
+        # window is closed.
+        return
+
+    with rasterio.drivers():
+        with rasterio.open('tests/data/RGB.byte.tif') as src:
+            try:
+                show((src, 1))
+            except ImportError:
+                pass
+
+            try:
+                show(src.read_band(1))
+            except ImportError:
+                pass
\ No newline at end of file

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/rasterio.git



More information about the Pkg-grass-devel mailing list