From 711f8fd45a07a7f6165dea8ca132560163935cbd Mon Sep 17 00:00:00 2001 From: Sean Gillies Date: Mon, 22 Dec 2014 18:18:50 -0800 Subject: [PATCH 1/2] --sequence means write JSON text sequences. --with-rs means add RS delimiters. Also, some parameter consolidation into a new params module. --- rasterio/rio/cli.py | 4 +- rasterio/rio/features.py | 61 +++++++-------------- rasterio/rio/params.py | 93 +++++++++++++++++++++++++++++-- rasterio/rio/rio.py | 71 +++++++++--------------- tests/test_cli.py | 115 --------------------------------------- tests/test_rio_rio.py | 14 +---- 6 files changed, 138 insertions(+), 220 deletions(-) delete mode 100644 tests/test_cli.py diff --git a/rasterio/rio/cli.py b/rasterio/rio/cli.py index cf54fda6..198ac049 100644 --- a/rasterio/rio/cli.py +++ b/rasterio/rio/cli.py @@ -44,12 +44,12 @@ def coords(obj): def write_features(file, collection, - agg_mode='obj', expression='feature', use_rs=False, + collect=True, expression='feature', use_rs=False, **dump_kwds): """Read an iterator of (feat, bbox) pairs and write to file using the selected modes.""" # Sequence of features expressed as bbox, feature, or collection. - if agg_mode == 'seq': + if not collect: for feat in collection(): xs, ys = zip(*coords(feat)) bbox = (min(xs), min(ys), max(xs), max(ys)) diff --git a/rasterio/rio/features.py b/rasterio/rio/features.py index ede426f8..b6095902 100644 --- a/rasterio/rio/features.py +++ b/rasterio/rio/features.py @@ -9,7 +9,9 @@ import click import rasterio from rasterio.transform import Affine from rasterio.rio.cli import cli, coords, write_features - +from rasterio.rio.params import ( + precision_opt, indent_opt, compact_opt, geographic_opt, projected_opt, + collection_opt, rs_opt, feature_mode_opt, bbox_mode_opt) warnings.simplefilter('default') @@ -17,39 +19,15 @@ warnings.simplefilter('default') # Shapes command. @cli.command(short_help="Write the shapes of features.") @click.argument('input', type=click.Path(exists=True)) -# Coordinate precision option. -@click.option('--precision', type=int, default=-1, - help="Decimal precision of coordinates.") -# JSON formatting options. -@click.option('--indent', default=None, type=int, - help="Indentation level for JSON output") -@click.option('--compact/--no-compact', default=False, - help="Use compact separators (',', ':').") -# Geographic (default) or Mercator switch. -@click.option('--geographic', 'projected', flag_value='geographic', - default=True, - help="Output in geographic coordinates (the default).") -@click.option('--projected', 'projected', flag_value='projected', - help="Output in projected coordinates.") -# JSON object (default) or sequence (experimental) switch. -@click.option('--json-obj', 'json_mode', flag_value='obj', default=True, - help="Write a single JSON object (the default).") -@click.option('--x-json-seq', 'json_mode', flag_value='seq', - help="Write a JSON sequence. Experimental.") -# Use ASCII RS control code to signal a sequence item (False is default). -# See http://tools.ietf.org/html/draft-ietf-json-text-sequence-05. -# Experimental. -@click.option('--x-json-seq-rs/--x-json-seq-no-rs', default=False, - help="Use RS as text separator. Experimental.") -# GeoJSON feature (default), bbox, or collection switch. Meaningful only -# when --x-json-seq is used. -@click.option('--collection', 'output_mode', flag_value='collection', - default=True, - help="Output as a GeoJSON feature collection (the default).") -@click.option('--feature', 'output_mode', flag_value='feature', - help="Output as sequence of GeoJSON features.") -@click.option('--bbox', 'output_mode', flag_value='bbox', - help="Output as a GeoJSON bounding box array.") +@precision_opt +@indent_opt +@compact_opt +@geographic_opt +@projected_opt +@collection_opt +@rs_opt +@feature_mode_opt(True) +@bbox_mode_opt(False) @click.option('--bands/--mask', default=True, help="Extract shapes from one of the dataset bands or from " "its nodata mask") @@ -61,17 +39,17 @@ warnings.simplefilter('default') help="Include or do not include (the default) nodata regions.") @click.pass_context def shapes( - ctx, input, precision, indent, compact, projected, json_mode, - x_json_seq_rs, output_mode, bands, bidx, sampling, with_nodata): + ctx, input, precision, indent, compact, projected, collection, + use_rs, output_mode, bands, bidx, sampling, with_nodata): """Writes features of a dataset out as GeoJSON. It's intended for use with single-band rasters and reads from the first band. """ - # These import numpy, which we don't want to do unless its needed. + # These import numpy, which we don't want to do unless it's needed. import numpy import rasterio.features import rasterio.warp - verbosity = ctx.obj['verbosity'] + verbosity = ctx.obj['verbosity'] if ctx.obj else 1 logger = logging.getLogger('rio') dump_kwds = {'sort_keys': True} if indent: @@ -144,11 +122,14 @@ def shapes( 'bbox': [min(xs), min(ys), max(xs), max(ys)], 'geometry': g } + if collection: + output_mode = 'collection' + try: with rasterio.drivers(CPL_DEBUG=verbosity>2): write_features( - stdout, Collection(), agg_mode=json_mode, - expression=output_mode, use_rs=x_json_seq_rs, + stdout, Collection(), collect=collection, + expression=output_mode, use_rs=use_rs, **dump_kwds) sys.exit(0) except Exception: diff --git a/rasterio/rio/params.py b/rasterio/rio/params.py index e282bf51..093cf649 100644 --- a/rasterio/rio/params.py +++ b/rasterio/rio/params.py @@ -1,6 +1,8 @@ +# Shared arguments and options. + import click - +# Common arguments. files_arg = click.argument( 'files', nargs=-1, @@ -8,11 +10,7 @@ files_arg = click.argument( required=True, metavar="INPUTS... OUTPUT") -format_opt = click.option( - '-f', '--format', '--driver', - default='GTiff', - help="Output format driver") - +# Common options. verbose_opt = click.option( '--verbose', '-v', count=True, @@ -22,3 +20,86 @@ quiet_opt = click.option( '--quiet', '-q', count=True, help="Decrease verbosity.") + +# Format driver option. +format_opt = click.option( + '-f', '--format', '--driver', + default='GTiff', + help="Output format driver") + +# JSON formatting options. +indent_opt = click.option( + '--indent', + type=int, + default=None, + help="Indentation level for JSON output") + +compact_opt = click.option( + '--compact/--no-compact', + default=False, + help="Use compact separators (',', ':').") + +# Coordinate precision option. +precision_opt = click.option( + '--precision', + type=int, + default=-1, + help="Decimal precision of coordinates.") + +# Geographic (default) or Mercator switch. +geographic_opt = click.option( + '--geographic', + 'projected', + flag_value='geographic', + default=True, + help="Output in geographic coordinates (the default).") + +projected_opt = click.option( + '--projected', + 'projected', + flag_value='projected', + help="Output in dataset's own, projected coordinates.") + +mercator_opt = click.option( + '--mercator', + 'projected', + flag_value='mercator', + help="Output in Web Mercator coordinates.") + +# Collection or sequence. +collection_opt = click.option( + '--collection/--sequence', + default=True, + help="Write a collection of shapes as a single object " + "(the default) or write a sequence of objects.") + +rs_opt = click.option( + '--with-rs/--without-rs', + 'use_rs', + default=False, + help="Use RS as text separator.") + +# GeoJSON output mode option. +def collection_mode_opt(default=False): + return click.option( + '--collection', + 'output_mode', + flag_value='collection', + default=default, + help="Output as sequence of GeoJSON feature collections.") + +def feature_mode_opt(default=False): + return click.option( + '--feature', + 'output_mode', + flag_value='feature', + default=default, + help="Output as sequence of GeoJSON features.") + +def bbox_mode_opt(default=False): + return click.option( + '--bbox', + 'output_mode', + flag_value='bbox', + default=default, + help="Output as a GeoJSON bounding box array.") diff --git a/rasterio/rio/rio.py b/rasterio/rio/rio.py index 10df9233..5a232302 100644 --- a/rasterio/rio/rio.py +++ b/rasterio/rio/rio.py @@ -13,7 +13,10 @@ import click import rasterio from rasterio.rio.cli import cli, write_features - +from rasterio.rio.params import ( + precision_opt, indent_opt, compact_opt, geographic_opt, projected_opt, + mercator_opt, collection_opt, rs_opt, feature_mode_opt, bbox_mode_opt, + collection_mode_opt) warnings.simplefilter('default') @@ -27,7 +30,11 @@ warnings.simplefilter('default') # Insp command. @cli.command(short_help="Open a data file and start an interpreter.") @click.argument('input', type=click.Path(exists=True)) -@click.option('--mode', type=click.Choice(['r', 'r+']), default='r', help="File mode (default 'r').") +@click.option( + '--mode', + type=click.Choice(['r', 'r+']), + default='r', + help="File mode (default 'r').") @click.pass_context def insp(ctx, input, mode): import rasterio.tool @@ -54,44 +61,20 @@ def insp(ctx, input, mode): # One or more files, the bounds of each are a feature in the collection # object or feature sequence. @click.argument('input', nargs=-1, type=click.Path(exists=True)) -# Coordinate precision option. -@click.option('--precision', type=int, default=-1, - help="Decimal precision of coordinates.") -# JSON formatting options. -@click.option('--indent', default=None, type=int, - help="Indentation level for JSON output") -@click.option('--compact/--no-compact', default=False, - help="Use compact separators (',', ':').") -# Geographic (default) or Mercator switch. -@click.option('--geographic', 'projected', flag_value='geographic', - default=True, - help="Output in geographic coordinates (the default).") -@click.option('--projected', 'projected', flag_value='projected', - help="Output in projected coordinates.") -@click.option('--mercator', 'projected', flag_value='mercator', - help="Output in Web Mercator coordinates.") -# JSON object (default) or sequence (experimental) switch. -@click.option('--json-obj', 'json_mode', flag_value='obj', default=True, - help="Write a single JSON object (the default).") -@click.option('--x-json-seq', 'json_mode', flag_value='seq', - help="Write a JSON sequence. Experimental.") -# Use ASCII RS control code to signal a sequence item (False is default). -# See http://tools.ietf.org/html/draft-ietf-json-text-sequence-05. -# Experimental. -@click.option('--x-json-seq-rs/--x-json-seq-no-rs', default=False, - help="Use RS as text separator. Experimental.") -# GeoJSON feature (default), bbox, or collection switch. Meaningful only -# when --x-json-seq is used. -@click.option('--collection', 'output_mode', flag_value='collection', - default=True, - help="Output as a GeoJSON feature collection (the default).") -@click.option('--feature', 'output_mode', flag_value='feature', - help="Output as sequence of GeoJSON features.") -@click.option('--bbox', 'output_mode', flag_value='bbox', - help="Output as a GeoJSON bounding box array.") +@precision_opt +@indent_opt +@compact_opt +@geographic_opt +@projected_opt +@mercator_opt +@collection_opt +@rs_opt +@collection_mode_opt(True) +@feature_mode_opt(False) +@bbox_mode_opt(False) @click.pass_context -def bounds(ctx, input, precision, indent, compact, projected, json_mode, - x_json_seq_rs, output_mode): +def bounds(ctx, input, precision, indent, compact, projected, collection, + use_rs, output_mode): """Write bounding boxes to stdout as GeoJSON for use with, e.g., geojsonio @@ -153,15 +136,14 @@ def bounds(ctx, input, precision, indent, compact, projected, json_mode, self._xs.extend(bbox[::2]) self._ys.extend(bbox[1::2]) - collection = Collection() - + col = Collection() # Use the generator defined above as input to the generic output # writing function. try: with rasterio.drivers(CPL_DEBUG=verbosity>2): write_features( - stdout, collection, agg_mode=json_mode, - expression=output_mode, use_rs=x_json_seq_rs, + stdout, col, collect=collection, + expression=output_mode, use_rs=use_rs, **dump_kwds) sys.exit(0) except Exception: @@ -174,8 +156,7 @@ def bounds(ctx, input, precision, indent, compact, projected, json_mode, @click.argument('input', default='-', required=False) @click.option('--src_crs', default='EPSG:4326', help="Source CRS.") @click.option('--dst_crs', default='EPSG:4326', help="Destination CRS.") -@click.option('--precision', type=int, default=-1, - help="Decimal precision of coordinates.") +@precision_opt @click.pass_context def transform(ctx, input, src_crs, dst_crs, precision): import rasterio.warp diff --git a/tests/test_cli.py b/tests/test_cli.py deleted file mode 100644 index 35cbabbd..00000000 --- a/tests/test_cli.py +++ /dev/null @@ -1,115 +0,0 @@ -import subprocess - - -def test_cli_bounds_obj_bbox(): - result = subprocess.check_output( - 'rio bounds tests/data/RGB.byte.tif --bbox --precision 6', - shell=True) - assert result.decode('utf-8').strip() == '[-78.898133, 23.564991, -76.599438, 25.550874]' - - -def test_cli_bounds_obj_bbox_mercator(): - result = subprocess.check_output( - 'rio bounds tests/data/RGB.byte.tif --bbox --mercator --precision 3', - shell=True) - assert result.decode('utf-8').strip() == '[-8782900.033, 2700489.278, -8527010.472, 2943560.235]' - - -def test_cli_bounds_obj_feature(): - result = subprocess.check_output( - 'rio bounds tests/data/RGB.byte.tif --feature --precision 6', - shell=True) - assert result.decode('utf-8').strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "0", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}' - - -def test_cli_bounds_obj_collection(): - result = subprocess.check_output( - 'rio bounds tests/data/RGB.byte.tif --precision 6', - shell=True) - assert result.decode('utf-8').strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "features": [{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "0", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}], "type": "FeatureCollection"}' - - -def test_cli_bounds_seq_feature_rs(): - result = subprocess.check_output( - 'rio bounds tests/data/RGB.byte.tif --x-json-seq --x-json-seq-rs --feature --precision 6', - shell=True) - assert result.decode('utf-8').startswith(u'\x1e') - assert result.decode('utf-8').strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "0", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}' - - -def test_cli_bounds_seq_collection(): - result = subprocess.check_output( - 'rio bounds tests/data/RGB.byte.tif --x-json-seq --x-json-seq-rs --precision 6', - shell=True) - assert result.decode('utf-8').startswith(u'\x1e') - assert result.decode('utf-8').strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "features": [{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "0", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}], "type": "FeatureCollection"}' - - -def test_cli_bounds_seq_bbox(): - result = subprocess.check_output( - 'rio bounds tests/data/RGB.byte.tif --x-json-seq --x-json-seq-rs --bbox --precision 6', - shell=True) - assert result.decode('utf-8').startswith(u'\x1e') - assert result.decode('utf-8').strip() == '[-78.898133, 23.564991, -76.599438, 25.550874]' - - -def test_cli_bounds_seq_collection_multi(tmpdir): - filename = str(tmpdir.join("test.json")) - tmp = open(filename, 'w') - - subprocess.check_call( - 'rio bounds tests/data/RGB.byte.tif tests/data/RGB.byte.tif --x-json-seq --x-json-seq-rs --precision 6', - stdout=tmp, - shell=True) - - tmp.close() - tmp = open(filename, 'r') - json_texts = [] - text = "" - for line in tmp: - rs_idx = line.find(u'\x1e') - if rs_idx >= 0: - if text: - text += line[:rs_idx] - json_texts.append(text) - text = line[rs_idx+1:] - else: - text += line - else: - json_texts.append(text) - - assert len(json_texts) == 2 - assert json_texts[0].strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "features": [{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "0", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}], "type": "FeatureCollection"}' - assert json_texts[1].strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "features": [{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "1", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}], "type": "FeatureCollection"}' - - -def test_cli_info_count(): - result = subprocess.check_output( - 'if [ `rio info tests/data/RGB.byte.tif --count` -eq 3 ]; ' - 'then echo "True"; fi', - shell=True) - assert result.decode('utf-8').strip() == 'True' - - -def test_cli_info_nodata(): - result = subprocess.check_output( - 'if [ `rio info tests/data/RGB.byte.tif --nodata` = "0.0" ]; ' - 'then echo "True"; fi', - shell=True) - assert result.decode('utf-8').strip() == 'True' - - -def test_cli_info_dtype(): - result = subprocess.check_output( - 'if [ `rio info tests/data/RGB.byte.tif --dtype` = "uint8" ]; ' - 'then echo "True"; fi', - shell=True) - assert result.decode('utf-8').strip() == 'True' - - -def test_cli_info_shape(): - result = subprocess.check_output( - 'if [[ `rio info tests/data/RGB.byte.tif --shape` == "718 791" ]]; ' - 'then echo "True"; fi', - shell=True, executable='/bin/bash') - assert result.decode('utf-8').strip() == 'True' diff --git a/tests/test_rio_rio.py b/tests/test_rio_rio.py index 4780e334..80b0a045 100644 --- a/tests/test_rio_rio.py +++ b/tests/test_rio_rio.py @@ -88,7 +88,7 @@ def test_bounds_seq(): runner = CliRunner() result = runner.invoke( rio.bounds, - ['tests/data/RGB.byte.tif', 'tests/data/RGB.byte.tif', '--x-json-seq', '--bbox', '--precision', '2']) + ['tests/data/RGB.byte.tif', 'tests/data/RGB.byte.tif', '--sequence', '--bbox', '--precision', '2']) assert result.exit_code == 0 assert result.output == '[-78.9, 23.56, -76.6, 25.55]\n[-78.9, 23.56, -76.6, 25.55]\n' assert '\x1e' not in result.output @@ -98,21 +98,11 @@ def test_bounds_seq_rs(): runner = CliRunner() result = runner.invoke( rio.bounds, - ['tests/data/RGB.byte.tif', 'tests/data/RGB.byte.tif', '--x-json-seq', '--x-json-seq-rs', '--bbox', '--precision', '2']) + ['tests/data/RGB.byte.tif', 'tests/data/RGB.byte.tif', '--sequence', '--with-rs', '--bbox', '--precision', '2']) assert result.exit_code == 0 assert result.output == '\x1e[-78.9, 23.56, -76.6, 25.55]\n\x1e[-78.9, 23.56, -76.6, 25.55]\n' - -def test_bounds_obj_feature(): - runner = CliRunner() - result = runner.invoke( - rio.bounds, - ['tests/data/RGB.byte.tif', '--feature', '--precision', '6']) - assert result.exit_code == 0 - assert result.output.strip() == '{"bbox": [-78.898133, 23.564991, -76.599438, 25.550874], "geometry": {"coordinates": [[[-78.898133, 23.564991], [-76.599438, 23.564991], [-76.599438, 25.550874], [-78.898133, 25.550874], [-78.898133, 23.564991]]], "type": "Polygon"}, "properties": {"id": "0", "title": "tests/data/RGB.byte.tif"}, "type": "Feature"}' - - def test_transform_err(): runner = CliRunner() result = runner.invoke( From c5267bb5f26bca1287a534a48ae426096979c599 Mon Sep 17 00:00:00 2001 From: Sean Gillies Date: Thu, 25 Dec 2014 15:44:37 -0800 Subject: [PATCH 2/2] Help updates for params. --- rasterio/rio/params.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rasterio/rio/params.py b/rasterio/rio/params.py index 093cf649..6d234b83 100644 --- a/rasterio/rio/params.py +++ b/rasterio/rio/params.py @@ -66,18 +66,21 @@ mercator_opt = click.option( flag_value='mercator', help="Output in Web Mercator coordinates.") -# Collection or sequence. +# Feature collection or feature sequence switch. collection_opt = click.option( '--collection/--sequence', default=True, - help="Write a collection of shapes as a single object " - "(the default) or write a sequence of objects.") + help="Write a single JSON text containing a feature collection object " + "(the default) or write a LF-delimited sequence of texts containing " + "individual objects.") rs_opt = click.option( '--with-rs/--without-rs', 'use_rs', default=False, - help="Use RS as text separator.") + help="Use RS (0x1E) as a prefix for individual texts in a sequence " + "as per http://tools.ietf.org/html/draft-ietf-json-text-sequence-13 " + "(default is False).") # GeoJSON output mode option. def collection_mode_opt(default=False):