diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..d3a89f1d --- /dev/null +++ b/.coveragerc @@ -0,0 +1,13 @@ +[run] +plugins = Cython.Coverage +source = rasterio +omit = *.pxd + +[report] +show_missing = True +exclude_lines = + pragma: no cover + def __repr__ + raise AssertionError + raise NotImplementedError + if __name__ == .__main__.: diff --git a/.travis.yml b/.travis.yml index 67b96685..73017285 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,10 +10,11 @@ env: - PIP_FIND_LINKS=file://$HOME/.cache/pip/wheels - GDALINST=$HOME/gdalinstall - GDALBUILD=$HOME/gdalbuild + - CYTHON_COVERAGE=1 matrix: - GDALVERSION = "1.9.2" - GDALVERSION = "1.11.2" - #- GDALVERSION = "2.0.1" + - GDALVERSION = "2.0.1" addons: apt: packages: @@ -37,9 +38,9 @@ install: - "pip wheel -r requirements-dev.txt" - "pip install -r requirements-dev.txt" - "pip install --upgrade --force-reinstall --global-option=build_ext --global-option='-I$GDALINST/gdal-$GDALVERSION/include' --global-option='-L$GDALINST/gdal-$GDALVERSION/lib' --global-option='-R$GDALINST/gdal-$GDALVERSION/lib' -e ." - - "pip install coveralls" + - "pip install coveralls>=1.1" - "pip install -e ." -script: +script: - py.test --cov rasterio --cov-report term-missing after_success: - coveralls diff --git a/CHANGES.txt b/CHANGES.txt index 03326b60..4f283416 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,15 @@ Changes ======= +0.29.0 (2015-10-22) +------------------- +- Fill masked arrays in rio-calc when using Numpy 1.10.x as well as with 1.8.x + (#500). +- When a raster dataset is not tiled, blockxszie and blockysize items are no + longer included in its `profile` property. This prevents meaningless block + size parameters from stripped, not tiled, datasets from being used when + creating new datasets (#503). + 0.28.0 (2015-10-06) ------------------- - Ensure that tools module is packaged (#489, #490). The rio-merge command was diff --git a/docs/windowed-rw.rst b/docs/windowed-rw.rst index d91f5a98..fe5d4979 100644 --- a/docs/windowed-rw.rst +++ b/docs/windowed-rw.rst @@ -161,6 +161,55 @@ This example also demonstrates decimation. :width: 500 :height: 300 + +Data windows +------------ + +Sometimes it is desirable to crop off an outer boundary of NODATA values around +a dataset: + +.. code-block:: python + + from rasterio import get_data_window + + with rasterio.open('tests/data/RGB.byte.tif') as src: + window = get_data_window(src.read(1, masked=True)) + # window = ((3, 714), (13, 770)) + + kwargs = src.meta.copy() + del kwargs['transform'] + kwargs.update({ + 'height': window[0][1] - window[0][0], + 'width': window[1][1] - window[1][0], + 'affine': src.window_transform(window) + }) + + with rasterio.open('/tmp/cropped.tif', 'w', **kwargs) as dst: + dst.write(src.read(window=window)) + + +Window utilities +---------------- + +Basic union and intersection operations are available for windows, to streamline +operations across dynamically created windows for a series of bands or datasets +with the same full extent. + +.. code-block:: python + + from rasterio import window_union, window_intersection + + # Full window is ((0, 1000), (0, 500)) + window1 = ((100, 500), (10, 500)) + window2 = ((10, 150), (50, 250)) + + outer = window_union([window1, window2]) + # outer = ((10, 500), (10, 500)) + + inner = window_intersection([window1, window2]) + # inner = ((100, 150), (50, 250)) + + Blocks ------ diff --git a/rasterio/__init__.py b/rasterio/__init__.py index 91b8bbf9..e9255436 100644 --- a/rasterio/__init__.py +++ b/rasterio/__init__.py @@ -23,7 +23,7 @@ from rasterio import _err, coords, enums __all__ = [ 'band', 'open', 'drivers', 'copy', 'pad'] -__version__ = "0.28.0" +__version__ = "0.29.0" log = logging.getLogger('rasterio') class NullHandler(logging.Handler): @@ -40,7 +40,6 @@ def open( crs=None, transform=None, dtype=None, nodata=None, - vfs=None, **kwargs): """Open file at ``path`` in ``mode`` "r" (read), "r+" (read/write), or "w" (write) and return a ``Reader`` or ``Updater`` object. @@ -95,11 +94,6 @@ def open( raise TypeError("invalid mode: %r" % mode) if driver and not isinstance(driver, string_types): raise TypeError("invalid driver: %r" % driver) - if vfs and not isinstance(vfs, string_types): - raise TypeError("invalid vfs: %r" % vfs) - - path, vsi, archive = parse_paths(path, vfs) - path = vsi_path(path, vsi=vsi, archive=archive) if transform: transform = guard_transform(transform) @@ -180,28 +174,81 @@ def pad(array, transform, pad_width, mode=None, **kwargs): return padded_array, Affine(*padded_trans[:6]) -def parse_paths(path, vfs=None): - archive = vsi = None - if vfs: - parts = vfs.split("://") - vsi = parts.pop(0) if parts else None - archive = parts.pop(0) if parts else None - else: - parts = path.split("://") - path = parts.pop() if parts else None - vsi = parts.pop() if parts else None - return path, vsi, archive +def get_data_window(arr, nodata=None): + """ + Returns a window for the non-nodata pixels within the input array. + + Parameters + ---------- + arr: numpy ndarray, <= 3 dimensions + nodata: number + If None, will either return a full window if arr is not a masked + array, or will use the mask to determine non-nodata pixels. + If provided, it must be a number within the valid range of the dtype + of the input array. + + Returns + ------- + ((row_start, row_stop), (col_start, col_stop)) + + """ + + from rasterio._io import get_data_window + return get_data_window(arr, nodata) -def vsi_path(path, vsi=None, archive=None): - # If a VSF and archive file are specified, we convert the path to - # a GDAL VSI path (see cpl_vsi.h). - if vsi: - path = path.strip(os.path.sep) - if archive: - result = os.path.sep.join(['/vsi{0}'.format(vsi), archive, path]) - else: - result = os.path.sep.join(['/vsi{0}'.format(vsi), path]) - else: - result = path - return result +def window_union(windows): + """ + Union windows and return the outermost extent they cover. + + Parameters + ---------- + windows: list-like of window objects + ((row_start, row_stop), (col_start, col_stop)) + + Returns + ------- + ((row_start, row_stop), (col_start, col_stop)) + """ + + from rasterio._io import window_union + return window_union(windows) + + +def window_intersection(windows): + """ + Intersect windows and return the innermost extent they cover. + + Will raise ValueError if windows do not intersect. + + Parameters + ---------- + windows: list-like of window objects + ((row_start, row_stop), (col_start, col_stop)) + + Returns + ------- + ((row_start, row_stop), (col_start, col_stop)) + """ + + from rasterio._io import window_intersection + return window_intersection(windows) + + +def windows_intersect(windows): + """ + Test if windows intersect. + + Parameters + ---------- + windows: list-like of window objects + ((row_start, row_stop), (col_start, col_stop)) + + Returns + ------- + boolean: + True if all windows intersect. + """ + + from rasterio._io import windows_intersect + return windows_intersect(windows) diff --git a/rasterio/_base.pyx b/rasterio/_base.pyx index a5ef3697..cb71e269 100644 --- a/rasterio/_base.pyx +++ b/rasterio/_base.pyx @@ -4,6 +4,7 @@ import logging import math +import os import sys import warnings @@ -30,6 +31,38 @@ else: log.addHandler(NullHandler()) +def parse_paths(url, vfs=None): + """Parse a file path or Apache VFS URL into its parts.""" + archive = scheme = None + if vfs: + parts = vfs.split("://") + vsi = parts.pop(0) if parts else None + archive = parts.pop(0) if parts else None + else: + parts = url.split("://") + path = parts.pop() if parts else None + scheme = parts.pop() if parts else None + if scheme in ('gzip', 'zip', 'tar'): + parts = path.split('!') + path = parts.pop() if parts else None + archive = parts.pop() if parts else None + return path, scheme, archive + + +def vsi_path(path, vsi=None, archive=None): + # If a VSF and archive file are specified, we convert the path to + # a GDAL VSI path (see cpl_vsi.h). + if vsi and vsi != 'file': + path = path.strip(os.path.sep) + if archive: + result = os.path.sep.join(['/vsi{0}'.format(vsi), archive, path]) + else: + result = os.path.sep.join(['/vsi{0}'.format(vsi), path]) + else: + result = path + return result + + cdef class DatasetReader(object): def __init__(self, path): @@ -62,7 +95,10 @@ cdef class DatasetReader(object): self.env = GDALEnv(False) self.env.start() - name_b = self.name.encode('utf-8') + path, vsi, archive = parse_paths(self.name) + path = vsi_path(path, vsi=vsi, archive=archive) + + name_b = path.encode('utf-8') cdef const char *fname = name_b with cpl_errs: self._hds = _gdal.GDALOpen(fname, 0) @@ -448,6 +484,10 @@ cdef class DatasetReader(object): else: return None + @property + def is_tiled(self): + return self.block_shapes[0][1] != self.width + property profile: """Basic metadata and creation options of this dataset. @@ -457,10 +497,13 @@ cdef class DatasetReader(object): def __get__(self): m = self.meta m.update(self.tags(ns='rio_creation_kwds')) - m.update( - blockxsize=self.block_shapes[0][1], - blockysize=self.block_shapes[0][0], - tiled=self.block_shapes[0][1] != self.width) + if self.is_tiled: + m.update( + blockxsize=self.block_shapes[0][1], + blockysize=self.block_shapes[0][0], + tiled=True) + else: + m.update(tiled=False) if self.compression: m['compress'] = self.compression.name if self.interleaving: diff --git a/rasterio/_io.pyx b/rasterio/_io.pyx index 1d42712c..f811153f 100644 --- a/rasterio/_io.pyx +++ b/rasterio/_io.pyx @@ -13,7 +13,8 @@ cimport numpy as np from rasterio cimport _base, _gdal, _ogr, _io from rasterio._base import ( - crop_window, eval_window, window_shape, window_index, tastes_like_gdal) + crop_window, eval_window, window_shape, window_index, tastes_like_gdal, + parse_paths) from rasterio._drivers import driver_count, GDALEnv from rasterio._err import cpl_errs from rasterio import dtypes @@ -1249,8 +1250,7 @@ cdef class RasterUpdater(RasterReader): cdef void *drv = NULL cdef void *hband = NULL cdef int success - name_b = self.name.encode('utf-8') - cdef const char *fname = name_b + # Is there not a driver manager already? if driver_count() == 0 and not self.env: @@ -1259,7 +1259,16 @@ cdef class RasterUpdater(RasterReader): else: self.env = GDALEnv(False) self.env.start() - + + path, scheme, archive = parse_paths(self.name) + if scheme and scheme != 'file': + raise TypeError( + "VFS '{0}' datasets can not be created or updated.".format( + scheme)) + + name_b = self.name.encode('utf-8') + cdef const char *fname = name_b + kwds = [] if self.mode == 'w': @@ -2002,6 +2011,12 @@ def writer(path, mode, **kwargs): cdef const char *drv_name = NULL cdef const char *fname = NULL + path, scheme, archive = parse_paths(path) + if scheme and scheme != 'file': + raise TypeError( + "VFS '{0}' datasets can not be created or updated.".format( + scheme)) + if mode == 'w' and 'driver' in kwargs: if kwargs['driver'] == 'GTiff': return RasterUpdater(path, mode, **kwargs) @@ -2041,3 +2056,133 @@ def virtual_file_to_buffer(filename): log.debug("Buffer length: %d bytes", n) cdef np.uint8_t[:] buff_view = buff return buff_view + + +def get_data_window(arr, nodata=None): + """ + Returns a window for the non-nodata pixels within the input array. + + Parameters + ---------- + arr: numpy ndarray, <= 3 dimensions + nodata: number + If None, will either return a full window if arr is not a masked + array, or will use the mask to determine non-nodata pixels. + If provided, it must be a number within the valid range of the dtype + of the input array. + + Returns + ------- + ((row_start, row_stop), (col_start, col_stop)) + + """ + + num_dims = len(arr.shape) + if num_dims > 3: + raise ValueError('get_data_window input array must have no more than ' + '3 dimensions') + + if nodata is None: + if not hasattr(arr, 'mask'): + return ((0, arr.shape[-2]), (0, arr.shape[-1])) + else: + arr = np.ma.masked_array(arr, arr == nodata) + + if num_dims == 2: + data_rows, data_cols = np.where(arr.mask == False) + else: + data_rows, data_cols = np.where( + np.any(np.rollaxis(arr.mask, 0, 3) == False, axis=2) + ) + + if data_rows.size: + row_range = (data_rows.min(), data_rows.max() + 1) + else: + row_range = (0, 0) + + if data_cols.size: + col_range = (data_cols.min(), data_cols.max() + 1) + else: + col_range = (0, 0) + + return (row_range, col_range) + + +def window_union(windows): + """ + Union windows and return the outermost extent they cover. + + Parameters + ---------- + windows: list-like of window objects + ((row_start, row_stop), (col_start, col_stop)) + + Returns + ------- + ((row_start, row_stop), (col_start, col_stop)) + """ + + + stacked = np.dstack(windows) + return ( + (stacked[0, 0].min(), stacked[0, 1].max()), + (stacked[1, 0].min(), stacked[1, 1]. max()) + ) + + +def window_intersection(windows): + """ + Intersect windows and return the innermost extent they cover. + + Will raise ValueError if windows do not intersect. + + Parameters + ---------- + windows: list-like of window objects + ((row_start, row_stop), (col_start, col_stop)) + + Returns + ------- + ((row_start, row_stop), (col_start, col_stop)) + """ + + if not windows_intersect(windows): + raise ValueError('windows do not intersect') + + stacked = np.dstack(windows) + return ( + (stacked[0, 0].max(), stacked[0, 1].min()), + (stacked[1, 0].max(), stacked[1, 1]. min()) + ) + + +def windows_intersect(windows): + """ + Test if windows intersect. + + Parameters + ---------- + windows: list-like of window objects + ((row_start, row_stop), (col_start, col_stop)) + + Returns + ------- + boolean: + True if all windows intersect. + """ + + from itertools import combinations + + def intersects(range1, range2): + return not ( + range1[0] > range2[1] or range1[1] < range2[0] + ) + + windows = np.array(windows) + + for i in (0, 1): + for c in combinations(windows[:, i], 2): + if not intersects(*c): + return False + + return True diff --git a/rasterio/rio/calc.py b/rasterio/rio/calc.py index 7018174e..b9df4ff3 100644 --- a/rasterio/rio/calc.py +++ b/rasterio/rio/calc.py @@ -123,8 +123,9 @@ def calc(ctx, command, files, output, name, dtype, masked, creation_options): res = snuggs.eval(command, **ctxkwds) - if (isinstance(res, np.ma.core.MaskedArray) and - tuple(LooseVersion(np.__version__).version) < (1, 9, 0)): + if (isinstance(res, np.ma.core.MaskedArray) and ( + tuple(LooseVersion(np.__version__).version) < (1, 9) or + tuple(LooseVersion(np.__version__).version) > (1, 10))): res = res.filled(kwargs['nodata']) if len(res.shape) == 3: diff --git a/rasterio/rio/main.py b/rasterio/rio/main.py index d2988f50..28bfb601 100644 --- a/rasterio/rio/main.py +++ b/rasterio/rio/main.py @@ -26,9 +26,8 @@ def configure_logging(verbosity): @cligj.verbose_opt @cligj.quiet_opt @click.version_option(version=rasterio.__version__, message='%(version)s') -@options.vfs_opt @click.pass_context -def main_group(ctx, verbose, quiet, vfs): +def main_group(ctx, verbose, quiet): """ Rasterio command line interface. @@ -38,4 +37,3 @@ def main_group(ctx, verbose, quiet, vfs): configure_logging(verbosity) ctx.obj = {} ctx.obj['verbosity'] = verbosity - ctx.obj['vfs'] = vfs diff --git a/rasterio/rio/merge.py b/rasterio/rio/merge.py index d816ac74..5e637c87 100644 --- a/rasterio/rio/merge.py +++ b/rasterio/rio/merge.py @@ -73,6 +73,7 @@ def merge(ctx, files, output, driver, bounds, res, nodata, force_overwrite, profile['height'] = dest.shape[1] profile['width'] = dest.shape[2] profile['driver'] = driver + profile.update(**creation_options) with rasterio.open(output, 'w', **profile) as dst: diff --git a/rasterio/rio/options.py b/rasterio/rio/options.py index a890f13c..88878694 100644 --- a/rasterio/rio/options.py +++ b/rasterio/rio/options.py @@ -50,7 +50,7 @@ import os.path import click -from rasterio import parse_paths, vsi_path +from rasterio._base import parse_paths def _cb_key_val(ctx, param, value): @@ -77,34 +77,27 @@ def _cb_key_val(ctx, param, value): raise click.BadParameter("Invalid syntax for KEY=VAL arg: {}".format(pair)) else: k, v = pair.split('=', 1) + k = k.lower() + v = v.lower() out[k] = v - return out -def vfs_handler(ctx, param, value): - if value: - path, vsi, archive = parse_paths(None, value) - if not vsi in ('gzip', 'zip', 'tar'): - raise click.BadParameter( - "VFS type {0} is unknown".format(vsi)) - if not os.path.exists(archive): - raise click.BadParameter( - "VFS archive {0} does not exist".format(archive)) - value = "{0}://{1}".format(vsi, os.path.abspath(archive)) - return value - - def file_in_handler(ctx, param, value): - vfs = (ctx.obj and ctx.obj.get('vfs')) - if vfs: - path, vsi, archive = parse_paths(value, vfs) - path = vsi_path(path, vsi, archive) + """Normalize ordinary filesystem and VFS paths""" + path, scheme, archive = parse_paths(value) + if scheme and not scheme in ('file', 'gzip', 'zip', 'tar'): + raise click.BadParameter( + "VFS type {0} is unknown".format(scheme)) + path_to_check = archive or path + if not os.path.exists(path_to_check): + raise click.BadParameter( + "Input file {0} does not exist".format(path_to_check)) + if archive and scheme: + archive = os.path.abspath(archive) + path = "{0}://{1}!{2}".format(scheme, archive, path) else: - if not os.path.exists(value): - raise click.BadParameter( - "Input file {0} does not exist".format(value)) - path = os.path.abspath(value) + path = os.path.abspath(path) return path @@ -188,11 +181,3 @@ rgb_opt = click.option( flag_value='rgb', default=False, help="Set RGB photometric interpretation.") - - -vfs_opt = click.option( - '--vfs', 'vfs', - default=None, - callback=vfs_handler, - help="Use a zip:// or tar:// archive as a virtual file system " - "('r' mode only).") diff --git a/requirements-dev.txt b/requirements-dev.txt index 563cdc12..12ad0fda 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,12 +1,11 @@ affine cligj -coveralls>=0.4 -cython>=0.23.1 +cython>=0.23.4 delocate enum34 -numpy>=1.8.0 +numpy>=1.8 snuggs>=1.2 pytest -pytest-cov +pytest-cov>=2.2.0 setuptools>=0.9.8 wheel diff --git a/requirements.txt b/requirements.txt index 0b59530e..90718100 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ affine cligj enum34 -numpy>=1.8.0 +numpy>=1.8 snuggs>=1.2 setuptools diff --git a/scripts/travis_gdal_install.sh b/scripts/travis_gdal_install.sh index e9454922..022dfd8f 100644 --- a/scripts/travis_gdal_install.sh +++ b/scripts/travis_gdal_install.sh @@ -60,30 +60,30 @@ ls -l $GDALINST if [ ! -d $GDALINST/gdal-1.9.2 ]; then cd $GDALBUILD wget http://download.osgeo.org/gdal/gdal-1.9.2.tar.gz - tar -xzvf gdal-1.9.2.tar.gz + tar -xzf gdal-1.9.2.tar.gz cd gdal-1.9.2 ./configure --prefix=$GDALINST/gdal-1.9.2 $GDALOPTS - make -j 2 + make -s -j 2 make install fi if [ ! -d $GDALINST/gdal-1.11.2 ]; then cd $GDALBUILD wget http://download.osgeo.org/gdal/1.11.2/gdal-1.11.2.tar.gz - tar -xzvf gdal-1.11.2.tar.gz + tar -xzf gdal-1.11.2.tar.gz cd gdal-1.11.2 ./configure --prefix=$GDALINST/gdal-1.11.2 $GDALOPTS - make -j 2 + make -s -j 2 make install fi if [ ! -d $GDALINST/gdal-2.0.1 ]; then cd $GDALBUILD wget http://download.osgeo.org/gdal/2.0.1/gdal-2.0.1.tar.gz - tar -xzvf gdal-2.0.1.tar.gz + tar -xzf gdal-2.0.1.tar.gz cd gdal-2.0.1 ./configure --prefix=$GDALINST/gdal-2.0.1 $GDALOPTS - make -j 2 + make -s -j 2 make install fi diff --git a/setup.cfg b/setup.cfg index 5eea52d4..5ee64771 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,8 +1,2 @@ -[nosetests] -tests=rasterio/tests -nocapture=True -verbosity=3 -logging-filter=rasterio -logging-level=DEBUG -with-coverage=1 -cover-package=rasterio +[pytest] +testpaths = tests diff --git a/setup.py b/setup.py index ed3b07f1..60789fd8 100755 --- a/setup.py +++ b/setup.py @@ -19,12 +19,10 @@ import sys from setuptools import setup from setuptools.extension import Extension + logging.basicConfig() log = logging.getLogger() -# python -W all setup.py ... -if 'all' in sys.warnoptions: - log.level = logging.DEBUG def check_output(cmd): # since subprocess.check_output doesn't exist in 2.6 @@ -39,6 +37,7 @@ def check_output(cmd): out, err = p.communicate() return out + def copy_data_tree(datadir, destdir): try: shutil.rmtree(destdir) @@ -46,6 +45,11 @@ def copy_data_tree(datadir, destdir): pass shutil.copytree(datadir, destdir) + +# python -W all setup.py ... +if 'all' in sys.warnoptions: + log.level = logging.DEBUG + # Parse the version from the rasterio module. with open('rasterio/__init__.py') as f: for line in f: @@ -135,6 +139,13 @@ if not os.name == "nt": ext_options['extra_compile_args'] = ['-Wno-unused-parameter', '-Wno-unused-function'] +cythonize_options = {} +if os.environ.get('CYTHON_COVERAGE'): + cythonize_options['compiler_directives'] = {'linetrace': True} + cythonize_options['annotate'] = True + ext_options['define_macros'] = [('CYTHON_TRACE', '1'), + ('CYTHON_TRACE_NOGIL', '1')] + log.debug('ext_options:\n%s', pprint.pformat(ext_options)) # When building from a repo, Cython is required. @@ -164,7 +175,7 @@ if os.path.exists("MANIFEST.in") and "clean" not in sys.argv: 'rasterio._err', ['rasterio/_err.pyx'], **ext_options), Extension( 'rasterio._example', ['rasterio/_example.pyx'], **ext_options), - ], quiet=True) + ], quiet=True, **cythonize_options) # If there's no manifest template, as in an sdist, we just specify .c files. else: @@ -193,12 +204,7 @@ with open('README.rst') as f: readme = f.read() # Runtime requirements. -inst_reqs = [ - 'affine>=1.0', - 'cligj>=0.2.0', - 'Numpy>=1.7', - 'snuggs>=1.3.1', - 'click-plugins'] +inst_reqs = ['affine', 'cligj', 'numpy', 'snuggs', 'click-plugins'] if sys.version_info < (3, 4): inst_reqs.append('enum34') diff --git a/tests/data/shade.tif b/tests/data/shade.tif index 6a5a2262..9ea0f3f0 100644 Binary files a/tests/data/shade.tif and b/tests/data/shade.tif differ diff --git a/tests/test_indexing.py b/tests/test_indexing.py index 257e7dcc..d42ca164 100644 --- a/tests/test_indexing.py +++ b/tests/test_indexing.py @@ -1,4 +1,13 @@ +import numpy +import pytest + import rasterio +from rasterio import ( + get_data_window, window_intersection, window_union, windows_intersect +) + + +DATA_WINDOW = ((3, 5), (2, 6)) def test_index(): @@ -67,3 +76,119 @@ def test_window_full_cover(): win = src.window(*bounds) bounds_calc = list(src.window_bounds(win)) assert bound_covers(bounds_calc, bounds) + + +@pytest.fixture +def data(): + data = numpy.zeros((10, 10), dtype='uint8') + data[slice(*DATA_WINDOW[0]), slice(*DATA_WINDOW[1])] = 1 + return data + + +def test_data_window_unmasked(data): + window = get_data_window(data) + assert window == ((0, data.shape[0]), (0, data.shape[1])) + + +def test_data_window_masked(data): + data = numpy.ma.masked_array(data, data == 0) + window = get_data_window(data) + assert window == DATA_WINDOW + + +def test_data_window_nodata(data): + window = get_data_window(data, nodata=0) + assert window == DATA_WINDOW + + window = get_data_window(numpy.ones_like(data), nodata=0) + assert window == ((0, data.shape[0]), (0, data.shape[1])) + + +def test_data_window_nodata_disjunct(): + data = numpy.zeros((3, 10, 10), dtype='uint8') + data[0, :4, 1:4] = 1 + data[1, 2:5, 2:8] = 1 + data[2, 1:6, 1:6] = 1 + window = get_data_window(data, nodata=0) + assert window == ((0, 6), (1, 8)) + + +def test_data_window_empty_result(): + data = numpy.zeros((3, 10, 10), dtype='uint8') + window = get_data_window(data, nodata=0) + assert window == ((0, 0), (0, 0)) + + +def test_data_window_masked_file(): + with rasterio.open('tests/data/RGB.byte.tif') as src: + window = get_data_window(src.read(1, masked=True)) + assert window == ((3, 714), (13, 770)) + + window = get_data_window(src.read(masked=True)) + assert window == ((3, 714), (13, 770)) + + +def test_window_union(): + assert window_union([ + ((0, 6), (3, 6)), + ((2, 4), (1, 5)) + ]) == ((0, 6), (1, 6)) + + +def test_window_intersection(): + assert window_intersection([ + ((0, 6), (3, 6)), + ((2, 4), (1, 5)) + ]) == ((2, 4), (3, 5)) + + assert window_intersection([ + ((0, 6), (3, 6)), + ((6, 10), (1, 5)) + ]) == ((6, 6), (3, 5)) + + assert window_intersection([ + ((0, 6), (3, 6)), + ((2, 4), (1, 5)), + ((3, 6), (0, 6)) + ]) == ((3, 4), (3, 5)) + + +def test_window_intersection_disjunct(): + with pytest.raises(ValueError): + window_intersection([ + ((0, 6), (3, 6)), + ((100, 200), (0, 12)), + ((7, 12), (7, 12)) + ]) + + +def test_windows_intersect(): + assert windows_intersect([ + ((0, 6), (3, 6)), + ((2, 4), (1, 5)) + ]) == True + + assert windows_intersect([ + ((0, 6), (3, 6)), + ((2, 4), (1, 5)), + ((3, 6), (0, 6)) + ]) == True + + +def test_windows_intersect_disjunct(): + assert windows_intersect([ + ((0, 6), (3, 6)), + ((10, 20), (0, 6)) + ]) == False + + assert windows_intersect([ + ((0, 6), (3, 6)), + ((2, 4), (1, 5)), + ((5, 6), (0, 6)) + ]) == False + + assert windows_intersect([ + ((0, 6), (3, 6)), + ((2, 4), (1, 3)), + ((3, 6), (4, 6)) + ]) == False \ No newline at end of file diff --git a/tests/test_options.py b/tests/test_options.py index 38ee698b..03bb15d2 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -7,7 +7,7 @@ def test_cb_key_val(): pairs = ['KEY=val', '1=='] expected = { - 'KEY': 'val', + 'key': 'val', '1': '=', } assert options._cb_key_val(None, None, pairs) == expected diff --git a/tests/test_profile.py b/tests/test_profile.py index 32979c4d..07cc2cf3 100644 --- a/tests/test_profile.py +++ b/tests/test_profile.py @@ -79,11 +79,26 @@ def test_profile_overlay(): assert kwds['count'] == 3 -def test_dataset_profile_property(data): +def test_dataset_profile_property_tiled(data): + """An tiled dataset's profile has block sizes""" + with rasterio.open('tests/data/shade.tif') as src: + assert src.profile['blockxsize'] == 256 + assert src.profile['blockysize'] == 256 + assert src.profile['tiled'] == True + + +def test_dataset_profile_property_untiled(data): + """An untiled dataset's profile has no block sizes""" + with rasterio.open('tests/data/RGB.byte.tif') as src: + assert 'blockxsize' not in src.profile + assert 'blockysize' not in src.profile + assert src.profile['tiled'] == False + + +def test_dataset_profile_creation_kwds(data): + """Updated creation keyword tags appear in profile""" tiffile = str(data.join('RGB.byte.tif')) with rasterio.open(tiffile, 'r+') as src: src.update_tags(ns='rio_creation_kwds', foo='bar') - assert src.profile['blockxsize'] == 791 - assert src.profile['blockysize'] == 3 assert src.profile['tiled'] == False assert src.profile['foo'] == 'bar' diff --git a/tests/test_vfs.py b/tests/test_vfs.py index 66d783e7..4b2ab1f6 100644 --- a/tests/test_vfs.py +++ b/tests/test_vfs.py @@ -1,15 +1,41 @@ import logging import sys +import pytest + import rasterio +from rasterio.profiles import default_gtiff_profile logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) -def test_read_zip(): +def test_read_vfs_zip(): with rasterio.open( - '/RGB.byte.tif', - vfs='zip://tests/data/files.zip') as src: - assert src.name == '/vsizip/tests/data/files.zip/RGB.byte.tif' + 'zip://tests/data/files.zip!/RGB.byte.tif') as src: + assert src.name == 'zip://tests/data/files.zip!/RGB.byte.tif' assert src.count == 3 + + +def test_read_vfs_file(): + with rasterio.open( + 'file://tests/data/RGB.byte.tif') as src: + assert src.name == 'file://tests/data/RGB.byte.tif' + assert src.count == 3 + + +def test_read_vfs_none(): + with rasterio.open( + 'tests/data/RGB.byte.tif') as src: + assert src.name == 'tests/data/RGB.byte.tif' + assert src.count == 3 + + +@pytest.mark.parametrize('mode', ['r+', 'w']) +def test_update_vfs(tmpdir, mode): + """VFS datasets can not be created or updated""" + with pytest.raises(TypeError): + _ = rasterio.open( + 'zip://{0}'.format(tmpdir), mode, + **default_gtiff_profile( + count=1, width=1, height=1))