Merge pull request #228 from mapbox/json-test-sequences

--sequence means write JSON text sequences.
This commit is contained in:
Brendan Ward 2014-12-27 06:10:40 -08:00
commit 13dc4752d7
6 changed files with 141 additions and 220 deletions

View File

@ -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))

View File

@ -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:

View File

@ -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,89 @@ 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.")
# Feature collection or feature sequence switch.
collection_opt = click.option(
'--collection/--sequence',
default=True,
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 (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):
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.")

View File

@ -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

View File

@ -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'

View File

@ -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(