Repository: opendatacube/datacube-core

Issue:
`get_bounds` reprojects dataset footprint 4 times more than necessary
This function:

https://github.com/opendatacube/datacube-core/blob/ee708f78c95c1049574221f09161560b50b67d7e/datacube/api/core.py#L661-L666

`d.extent.to_crs(crs).boundingbox` is not a cheap operation to repeat 4 times, under the hood it does:

1. Inject extra points into `extent` polygon
2. Reproject all the points to a new crs
3. Compute min/max on points




Discussion:

Resolving patch:
diff --git a/datacube/api/core.py b/datacube/api/core.py
--- a/datacube/api/core.py
+++ b/datacube/api/core.py
@@ -659,11 +659,8 @@ def _fuse_measurement(dest, datasets, geobox, measurement,
 
 
 def get_bounds(datasets, crs):
-    left = min([d.extent.to_crs(crs).boundingbox.left for d in datasets])
-    right = max([d.extent.to_crs(crs).boundingbox.right for d in datasets])
-    top = max([d.extent.to_crs(crs).boundingbox.top for d in datasets])
-    bottom = min([d.extent.to_crs(crs).boundingbox.bottom for d in datasets])
-    return geometry.box(left, bottom, right, top, crs=crs)
+    bbox = geometry.bbox_union(ds.extent.to_crs(crs).boundingbox for ds in datasets)
+    return geometry.box(*bbox, crs=crs)
 
 
 def dataset_type_to_row(dt):
diff --git a/datacube/utils/geometry/__init__.py b/datacube/utils/geometry/__init__.py
--- a/datacube/utils/geometry/__init__.py
+++ b/datacube/utils/geometry/__init__.py
@@ -8,6 +8,7 @@
     CRS,
     Geometry,
     GeoBox,
+    bbox_union,
     intersects,
     scaled_down_geobox,
     point,
@@ -60,6 +61,7 @@
     "CRS",
     "Geometry",
     "GeoBox",
+    "bbox_union",
     "intersects",
     "point",
     "multipoint",
diff --git a/datacube/utils/geometry/_base.py b/datacube/utils/geometry/_base.py
--- a/datacube/utils/geometry/_base.py
+++ b/datacube/utils/geometry/_base.py
@@ -1,7 +1,7 @@
 import functools
 import math
 from collections import namedtuple, OrderedDict
-from typing import Tuple, Callable
+from typing import Tuple, Callable, Iterable
 
 import cachetools
 import numpy
@@ -958,3 +958,21 @@ def _round_to_res(value, res, acc=0.1):
 
 def intersects(a, b):
     return a.intersects(b) and not a.touches(b)
+
+
+def bbox_union(bbs: Iterable[BoundingBox]) -> BoundingBox:
+    """ Given a stream of bounding boxes compute enclosing BoundingBox
+    """
+    # pylint: disable=invalid-name
+
+    L = B = float('+inf')
+    R = T = float('-inf')
+
+    for bb in bbs:
+        l, b, r, t = bb
+        L = min(l, L)
+        B = min(b, B)
+        R = max(r, R)
+        T = max(t, T)
+
+    return BoundingBox(L, B, R, T)


Test patch:
diff --git a/tests/test_geometry.py b/tests/test_geometry.py
--- a/tests/test_geometry.py
+++ b/tests/test_geometry.py
@@ -7,6 +7,8 @@
 from datacube.utils import geometry
 from datacube.utils.geometry import (
     GeoBox,
+    BoundingBox,
+    bbox_union,
     decompose_rws,
     affine_from_pts,
     get_scale_at_point,
@@ -174,6 +176,20 @@ def test_ops():
     assert pt.interpolate(3) is None
 
 
+def test_bbox_union():
+    b1 = BoundingBox(0, 1, 10, 20)
+    b2 = BoundingBox(5, 6, 11, 22)
+
+    assert bbox_union([b1]) == b1
+    assert bbox_union([b2]) == b2
+
+    bb = bbox_union(iter([b1, b2]))
+    assert bb == BoundingBox(0, 1, 11, 22)
+
+    bb = bbox_union(iter([b2, b1]*10))
+    assert bb == BoundingBox(0, 1, 11, 22)
+
+
 def test_unary_union():
     box1 = geometry.box(10, 10, 30, 30, crs=epsg4326)
     box2 = geometry.box(20, 10, 40, 30, crs=epsg4326)