Repository: rasterio/rasterio

Issue:
empty window intersection
I've been playing around with the cool windowing functions @brendan-ward put in a while back.  I'm wondering if this is the desired behavior when two windows intersect at a point or along a row/column.  Because of the ways windows work in rasterio, the upper row/column bound is open, so `windows_intersect` is returning `True` but returning an empty array, e.g.

```
>>> with rasterio.open('tests/data/RGB.byte.tif') as src:
...     w1 = ((0, 2), (0, 2))
...     w2 = ((2, 4), (2, 4))
...    if windows_intersect((w1, w2)): #returns True
...        print(src.read(1, window=window_intersection((w1, w2))))
[]
```

True, the windows do intersect at a point, but I assume the way people will use `window_intersection` is to expect a window that they can use in further processing. 



Discussion:
I'd say `intersection` is behaving as expected.

But `intersects` could be changed to reflect this case.  Instead of a strict geometric intersection, we could change the semantics to "produces a non-empty window intersection".

@sgillies @brendan-ward I'm not sure if there would be other implications to this beside maybe changing a few tests.

@grovduck side note: as of version 0.34 those functions have moved to the `rasterio.windows` module. The other functions will  continue working but give a deprecation warning and will disappear in 1.0.

@grovduck thanks for the report. I think that it's reasonable to expect `windows_intersect` and `windows_intersection` to be consistent, e.g. `windows_intersect((w1, w2)) == bool(windows_intersection((w1, w2)))` should evaluate to `True`, and that we should make it so.

Thanks, @perrygeo, @sgillies.  Yep, I agree that `intersection` is behaving as expected.  I started wondering if the term **overlaps** (or something like that) may be more appropriate in this case.  Probably not worth changing the existing API, but clarification would help.

Thanks for the heads up on the `windows` module.  I was looking at @brendan-ward's original changeset when I was testing.


Resolving patch:
diff --git a/rasterio/windows.py b/rasterio/windows.py
--- a/rasterio/windows.py
+++ b/rasterio/windows.py
@@ -1,5 +1,21 @@
 """Windows and related functions."""
 
+import functools
+import collections
+
+
+def iter_args(function):
+    """Decorator to allow function to take either *args or
+    a single iterable which gets expanded to *args.
+    """
+    @functools.wraps(function)
+    def wrapper(*args, **kwargs):
+        if len(args) == 1 and isinstance(args[0], collections.Iterable):
+            return function(*args[0])
+        else:
+            return function(*args)
+    return wrapper
+
 
 def get_data_window(arr, nodata=None):
     """Return a window for the non-nodata pixels within the input array.
@@ -22,7 +38,8 @@ def get_data_window(arr, nodata=None):
     return get_data_window(arr, nodata)
 
 
-def union(windows):
+@iter_args
+def union(*windows):
     """Union windows and return the outermost extent they cover.
 
     Parameters
@@ -38,7 +55,8 @@ def union(windows):
     return window_union(windows)
 
 
-def intersection(windows):
+@iter_args
+def intersection(*windows):
     """Intersect windows and return the innermost extent they cover.
 
     Will raise ValueError if windows do not intersect.
@@ -56,7 +74,8 @@ def intersection(windows):
     return window_intersection(windows)
 
 
-def intersect(windows):
+@iter_args
+def intersect(*windows):
     """Test if windows intersect.
 
     Parameters


Test patch:
diff --git a/tests/test_indexing.py b/tests/test_indexing.py
--- a/tests/test_indexing.py
+++ b/tests/test_indexing.py
@@ -126,66 +126,121 @@ def test_data_window_masked_file():
 
 
 def test_window_union():
-    assert windows.union([
+    assert windows.union(
         ((0, 6), (3, 6)),
         ((2, 4), (1, 5))
-    ]) == ((0, 6), (1, 6))
+    ) == ((0, 6), (1, 6))
 
 
 def test_window_intersection():
-    assert windows.intersection([
+    assert windows.intersection(
         ((0, 6), (3, 6)),
         ((2, 4), (1, 5))
-    ]) == ((2, 4), (3, 5))
+    ) == ((2, 4), (3, 5))
 
-    assert windows.intersection([
-        ((0, 6), (3, 6)),
-        ((6, 10), (1, 5))
-    ]) == ((6, 6), (3, 5))
-
-    assert windows.intersection([
+    assert windows.intersection(
         ((0, 6), (3, 6)),
         ((2, 4), (1, 5)),
         ((3, 6), (0, 6))
-    ]) == ((3, 4), (3, 5))
+    ) == ((3, 4), (3, 5))
 
 
 def test_window_intersection_disjunct():
     with pytest.raises(ValueError):
-        windows.intersection([
+        windows.intersection(
             ((0, 6), (3, 6)),
             ((100, 200), (0, 12)),
-            ((7, 12), (7, 12))
-        ])
+            ((7, 12), (7, 12)))
+
+        # touch, no overlap on edge of open interval
+        assert windows.intersection(
+            ((0, 6), (3, 6)),
+            ((6, 10), (1, 5)))
 
 
 def test_windows_intersect():
-    assert windows.intersect([
+    assert windows.intersect(
         ((0, 6), (3, 6)),
-        ((2, 4), (1, 5))
-    ]) == True
+        ((2, 4), (1, 5))) is True
 
-    assert windows.intersect([
+    assert windows.intersect(
         ((0, 6), (3, 6)),
         ((2, 4), (1, 5)),
-        ((3, 6), (0, 6))
-    ]) == True
+        ((3, 6), (0, 6))) is True
+
+    assert windows.intersect(
+        ((0, 2), (0, 2)),
+        ((1, 4), (1, 4))) is True
+
+
+def test_3x3matrix():
+    """For a 3x3 arrangement of 2x2 windows
+      a | b | c
+      ---------
+      d | e | f
+      ---------
+      g | h | i
+
+      i.e. window e is ((2, 4), (2, 4))
+
+      None of them should intersect or have an intersection
+    """
+    from itertools import product, combinations
+
+    pairs = ((0, 2), (2, 4), (4, 6))
+    arrangement = product(pairs, pairs)
+    for wins in combinations(arrangement, 2):
+        assert not windows.intersect(*wins)
+        with pytest.raises(ValueError):
+            windows.intersection(*wins)
 
 
 def test_windows_intersect_disjunct():
-    assert windows.intersect([
+    assert windows.intersect(
         ((0, 6), (3, 6)),
-        ((10, 20), (0, 6))
-    ]) == False
+        ((10, 20), (0, 6))) is False
 
-    assert windows.intersect([
+    # polygons touch at point
+    assert windows.intersect(
+        ((0, 2), (1, 3)),
+        ((2, 4), (3, 5))) is False
+
+    # polygons touch at point, rev order
+    assert windows.intersect(
+        ((2, 4), (3, 5)),
+        ((0, 2), (1, 3))) is False
+
+    # polygons touch at line
+    assert windows.intersect(
+        ((0, 6), (3, 6)),
+        ((6, 10), (1, 5))) is False
+
+    assert windows.intersect(
         ((0, 6), (3, 6)),
         ((2, 4), (1, 5)),
-        ((5, 6), (0, 6))
-    ]) == False
+        ((5, 6), (0, 6))) is False
 
-    assert windows.intersect([
+    assert windows.intersect(
         ((0, 6), (3, 6)),
         ((2, 4), (1, 3)),
-        ((3, 6), (4, 6))
-    ]) == False
+        ((3, 6), (4, 6))) is False
+
+
+def test_iter_args_winfuncs():
+    wins = [
+        ((0, 6), (3, 6)),
+        ((2, 4), (1, 5))]
+
+    assert windows.intersect(*wins) == windows.intersect(wins)
+    assert windows.intersection(*wins) == windows.intersection(wins)
+    assert windows.union(*wins) == windows.union(wins)
+
+
+def test_iter_args():
+    from rasterio.windows import iter_args
+
+    @iter_args
+    def foo(*args):
+        return len(args)
+
+    assert foo([0, 1, 2]) == foo(0, 1, 2) == foo(range(3)) == 3