Repository: rasterio/rasterio

Issue:
WarpedVRT does not permit boundless reads (sample.py)
## Expected behavior and actual behavior.

```
    def test_rasterio_vrt(self):

        import rasterio

    

        # tmp_file default crs is UTM: CRS({'init': 'epsg:32618'}

        with create_tmp_geotiff() as (tmp_file, expected):

            with rasterio.open(tmp_file) as src:

                with rasterio.vrt.WarpedVRT(src, crs="epsg:4326") as vrt:

                    expected_shape = (vrt.width, vrt.height)

                    expected_crs = vrt.crs

                    expected_res = vrt.res

                    # Value of single pixel in center of image

                    lon, lat = vrt.xy(vrt.width // 2, vrt.height // 2)

>                   expected_val = next(vrt.sample([(lon, lat)]))

test/integration/test_integration__io.py:799: 

_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

../../../miniconda/envs/test/lib/python3.7/site-packages/rasterio/sample.py:43: in sample_gen

    data = read(indexes, window=window, masked=masked, boundless=True)

_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

>   ???

E   ValueError: WarpedVRT does not permit boundless reads
```

## Operating system

For example: Mac OS, Windows, Linux

## Rasterio version and provenance

1.1.1 from conda-forge


Discussion:
Looks like this is likely the change: https://github.com/mapbox/rasterio/commit/7a1d295dfb51d9ba9f46188f45d612c187d2f653#diff-295d7ab62345d7c96b99de41201b1f62
How would you like to handle it? Should `sample_gen` check if the `dataset` is a `WarpedVRT`?
@snowman2 something we should do is check whether the Cython bug that required me to implement sample_gen separately (#378) has been fixed. If it has, we can roll sample_gen into dataset.sample(), with a separate implementation for the WarpedVRT class.

Resolving patch:
diff --git a/rasterio/sample.py b/rasterio/sample.py
--- a/rasterio/sample.py
+++ b/rasterio/sample.py
@@ -2,6 +2,7 @@
 
 import numpy
 
+from rasterio.enums import MaskFlags
 from rasterio.windows import Window
 
 
@@ -31,14 +32,24 @@ def sample_gen(dataset, xy, indexes=None, masked=False):
     index = dataset.index
     read = dataset.read
 
-    if isinstance(indexes, int):
+    if indexes is None:
+        indexes = dataset.indexes
+    elif isinstance(indexes, int):
         indexes = [indexes]
 
     for x, y in xy:
+
         row_off, col_off = index(x, y)
-#        if row_off < 0 or col_off < 0:
-#            yield numpy.ones((dataset.count,), dtype=dataset.dtypes[0]) * dataset.nodata
-#        else:
-        window = Window(col_off, row_off, 1, 1)
-        data = read(indexes, window=window, masked=masked, boundless=True)
-        yield data[:, 0, 0]
+
+        if row_off < 0 or col_off < 0 or row_off >= dataset.height or col_off >= dataset.width:
+            data = numpy.ones((len(indexes),), dtype=dataset.dtypes[0]) * (dataset.nodata or 0)
+            if masked:
+                mask = [False if MaskFlags.all_valid in dataset.mask_flag_enums[i - 1] else True for i in indexes]
+                yield numpy.ma.array(data, mask=mask)
+            else:
+                yield data
+
+        else:
+            window = Window(col_off, row_off, 1, 1)
+            data = read(indexes, window=window, masked=masked)
+            yield data[:, 0, 0]


Test patch:
diff --git a/tests/test_warpedvrt.py b/tests/test_warpedvrt.py
--- a/tests/test_warpedvrt.py
+++ b/tests/test_warpedvrt.py
@@ -512,6 +512,19 @@ def test_out_dtype(red_green):
             assert image[32, 32, 1] == 17.0
 
 
+def test_sample(red_green):
+    """See https://github.com/mapbox/rasterio/issues/1833."""
+    with rasterio.Env():
+        with rasterio.open(str(red_green.join("red.tif"))) as src, WarpedVRT(
+            src,
+            transform=affine.Affine.translation(-src.width / 4, src.height / 4) * src.transform,
+            width=2 * src.width,
+            height=2 * src.height
+        ) as vrt:
+            sample = next(vrt.sample([(-20, -50)]))
+            assert not sample.any()
+
+
 @pytest.fixture
 def dsrec(capfd):
     """GDAL's open dataset records as a pytest fixture"""