Repository: rasterio/rasterio

Issue:
Automatic computation of zoom levels for rio overwiew
Since GDAL 2.3 in gdaladdo it is [possible](https://www.gdal.org/gdaladdo.html) to omit the list of overview factors. But it looks like `rio overwiew` doesn't support this.


Discussion:
@drnextgis thanks for pointing this out. I'm not going to assign it to a milestone until I get a few other bugs closed up.
we implemented this in rio-cogeo https://github.com/vincentsarago/rio-cogeo/blob/6a2ac3705c15f943588513054585b525e61d8b7c/rio_cogeo/utils.py#L6 it will be great when taking care of the issue if we can create a public function to replace the one used in rio-cogeo 
@sgillies I can try to submit a PR on this next week. 

Resolving patch:
diff --git a/rasterio/rio/overview.py b/rasterio/rio/overview.py
--- a/rasterio/rio/overview.py
+++ b/rasterio/rio/overview.py
@@ -19,19 +19,52 @@ def build_handler(ctx, param, value):
                 base, exp_range = value.split('^')
                 exp_min, exp_max = (int(v) for v in exp_range.split('..'))
                 value = [pow(int(base), k) for k in range(exp_min, exp_max + 1)]
-            else:
+            elif ',' in value:
                 value = [int(v) for v in value.split(',')]
+            elif value == "auto":
+                pass
+            else:
+                raise Exception
         except Exception:
-            raise click.BadParameter(u"must match 'n,n,n,…' or 'n^n..n'.")
+            raise click.BadParameter(u"must match 'n,n,n,…', 'n^n..n', or 'auto'.")
     return value
 
 
+def get_maximum_overview_level(width, height, minsize=256):
+    """
+    Calculate the maximum overview level of a dataset at which
+    the smallest overview is smaller than `minsize`.
+
+    Attributes
+    ----------
+    width : int
+        Width of the dataset.
+    height : int
+        Height of the dataset.
+    minsize : int (default: 256)
+        Minimum overview size.
+
+    Returns
+    -------
+    overview_level: int
+        overview level.
+
+    """
+    overview_level = 0
+    overview_factor = 1
+    while min(width // overview_factor, height // overview_factor) > minsize:
+        overview_factor *= 2
+        overview_level += 1
+
+    return overview_level
+
+
 @click.command('overview', short_help="Construct overviews in an existing dataset.")
 @options.file_in_arg
-@click.option('--build', callback=build_handler, metavar=u"f1,f2,…|b^min..max",
-              help="A sequence of decimation factors specied as "
+@click.option('--build', callback=build_handler, metavar=u"f1,f2,…|b^min..max|auto",
+              help="A sequence of decimation factors specified as "
                    "comma-separated list of numbers or a base and range of "
-                   "exponents.")
+                   "exponents, or 'auto' to automatically determine the maximum factor.")
 @click.option('--ls', help="Print the overviews for each band.",
               is_flag=True, default=False)
 @click.option('--rebuild', help="Reconstruct existing overviews.",
@@ -52,10 +85,15 @@ def overview(ctx, input, build, ls, rebuild, resampling):
 
       rio overview --build 2,4,8,16
 
-    or a base and range of exponents.
+    or a base and range of exponents
 
       rio overview --build 2^1..4
 
+    or 'auto' to automatically determine the maximum decimation level at
+    which the smallest overview is smaller than 256 pixels in size.
+
+      rio overview --build auto
+
     Note that overviews can not currently be removed and are not
     automatically updated when the dataset's primary bands are
     modified.
@@ -92,6 +130,9 @@ def overview(ctx, input, build, ls, rebuild, resampling):
 
         elif build:
             with rasterio.open(input, 'r+') as dst:
+                if build == "auto":
+                    overview_level = get_maximum_overview_level(dst.width, dst.height)
+                    build = [2 ** j for j in range(1, overview_level + 1)]
                 dst.build_overviews(build, Resampling[resampling])
 
                 # Save the resampling method to a tag.


Test patch:
diff --git a/tests/test_rio_overview.py b/tests/test_rio_overview.py
--- a/tests/test_rio_overview.py
+++ b/tests/test_rio_overview.py
@@ -2,9 +2,11 @@
 import sys
 
 from click.testing import CliRunner
+import pytest
 
 import rasterio
 from rasterio.rio.main import main_group as cli
+from rasterio.rio.overview import get_maximum_overview_level
 
 
 logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
@@ -73,4 +75,28 @@ def test_no_args(data):
     inputfile = str(data.join('RGB.byte.tif'))
     result = runner.invoke(cli, ['overview', inputfile])
     assert result.exit_code == 2
-    assert "Please specify --ls, --rebuild, or --build ..." in result.output
+
+
+def test_build_auto_ls(data):
+    runner = CliRunner()
+    inputfile = str(data.join('RGB.byte.tif'))
+    result = runner.invoke(cli, ['overview', inputfile, '--build', 'auto'])
+    assert result.exit_code == 0
+    result = runner.invoke(cli, ['overview', inputfile, '--ls'])
+    assert result.exit_code == 0
+    expected = "  Band 1: [2, 4] (method: 'nearest')\n  Band 2: [2, 4] (method: 'nearest')\n  Band 3: [2, 4] (method: 'nearest')\n"
+    assert result.output.endswith(expected)
+
+
+@pytest.mark.parametrize(
+    "width, height, minsize, expected",
+    [
+        (256, 256, 256, 0),
+        (257, 257, 256, 1),
+        (1000, 1000, 128, 3),
+        (1000, 100, 128, 0)
+    ]
+)
+def test_max_overview(width, height, minsize, expected):
+    overview_level = get_maximum_overview_level(width, height, minsize)
+    assert overview_level == expected