Skip to content

Commit

Permalink
Add a rio create command. (#3023)
Browse files Browse the repository at this point in the history
* Add a rio create command.

Resolves #3021.

* Eliminate deprecated pytest.warns usage

* Add CLI doc for rio create

* Fix punctuation.

* Add bounds option for geotransform, remove duplicate nodata option.

* Warn about duplicate georeferencing
  • Loading branch information
sgillies committed Feb 8, 2024
1 parent 34b0d1b commit 35a5906
Show file tree
Hide file tree
Showing 9 changed files with 756 additions and 74 deletions.
1 change: 1 addition & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Deprecations:

New Features:

- The new "rio create" command allows creation of new, empty datasets (#3023).
- An optional range keyword argument (like that of numpy.histogram()) has been
added to show_hist() (#2873, #3001).
- Datasets stored in proprietary systems or addressable only through protocols
Expand Down
29 changes: 25 additions & 4 deletions docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace and handling of context variables.

.. code-block:: console
$ rio --help
$ rio --help
Usage: rio [OPTIONS] COMMAND [ARGS]...
Rasterio command line interface.
Expand All @@ -28,6 +28,7 @@ namespace and handling of context variables.
--aws-requester-pays Requester pays data transfer costs
--version Show the version and exit.
--gdal-version
--show-versions Show dependency versions
--help Show this message and exit.
Commands:
Expand All @@ -36,6 +37,7 @@ namespace and handling of context variables.
calc Raster data calculator.
clip Clip a raster to given bounds.
convert Copy and convert raster dataset.
create Create an empty or filled dataset.
edit-info Edit dataset metadata.
env Print information about the Rasterio environment.
gcps Print ground control points as GeoJSON.
Expand All @@ -52,11 +54,9 @@ namespace and handling of context variables.
transform Transform coordinates.
warp Warp a raster dataset.
Commands are shown below. See ``--help`` of individual commands for more
details.


creation options
----------------

Expand Down Expand Up @@ -161,7 +161,6 @@ use with, e.g., `geojsonio-cli <https://github.com/mapbox/geojsonio-cli>`__.
Shoot the GeoJSON into a Leaflet map using geojsonio-cli by typing
``rio bounds tests/data/RGB.byte.tif | geojsonio``.


calc
----

Expand Down Expand Up @@ -245,6 +244,28 @@ as uint8:
You can use `--rgb` as shorthand for `--co photometric=rgb`.

create
------

The ``create`` command creates an empty dataset.

The fundamental, required parameters are: format driver name, data type, count
of bands, height and width in pixels. Long and short options are provided for
each of these. Coordinate reference system and affine transformation matrix are
not strictly required and have long options only. All other format specific
creation outputs must be specified using the --co option.

The pixel values of an empty dataset are format specific. "Smart" formats like
GTiff use 0 or the nodata value if provided.

For example:

.. code-block:: console
$ rio create new.tif -f GTiff -t uint8 -n 3 -h 512 -w 512 \
> --co tiled=true --co blockxsize=256 --co blockysize=256
The command above produces a 3-band GeoTIFF with 256 x 256 internal tiling.

edit-info
---------
Expand Down
1 change: 1 addition & 0 deletions rasterio/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class NotGeoreferencedWarning(UserWarning):
class TransformWarning(UserWarning):
"""Warn that coordinate transformations may behave unexpectedly"""


class RPCError(ValueError):
"""Raised when RPC transformation is invalid"""

Expand Down
166 changes: 166 additions & 0 deletions rasterio/rio/create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""The rio create command."""

import click
import json
import os

import rasterio
from rasterio.crs import CRS
from rasterio.errors import (
CRSError,
FileOverwriteError,
RasterioIOError,
)
from rasterio.rio import options
from rasterio.transform import Affine, guard_transform


def crs_handler(ctx, param, value):
"""Get crs value from the command line."""
retval = None
if value is not None:
try:
retval = CRS.from_string(value)
except CRSError:
raise click.BadParameter(
f"{value} is not a recognized CRS.", param=param, param_hint="crs"
)
return retval


def transform_handler(ctx, param, value):
"""Get transform value from the command line."""
retval = None
if value is not None:
try:
value = json.loads(value)
retval = guard_transform(value)
except Exception:
raise click.BadParameter(
f"{value} is not recognized as a transformarray.",
param=param,
param_hint="transform",
)
return retval


@click.command(short_help="Create an empty or filled dataset.")
@options.file_out_arg
@options.format_opt
@options.dtype_opt
@click.option("--count", "-n", type=int, help="Number of raster bands.")
@click.option("--height", "-h", type=int, help="Raster height, or number of rows.")
@click.option("--width", "-w", type=int, help="Raster width, or number of columns.")
@options.nodata_opt
@click.option(
"--crs", callback=crs_handler, default=None, help="Coordinate reference system."
)
@click.option(
"--transform",
callback=transform_handler,
help="Affine transform matrix. Overrides any given bounds option.",
)
@options.bounds_opt
@options.overwrite_opt
@options.creation_options
@click.pass_context
def create(
ctx,
output,
driver,
dtype,
count,
height,
width,
nodata,
crs,
transform,
bounds,
overwrite,
creation_options,
):
"""Create an empty dataset.
The fundamental, required parameters are: format driver name, data
type, count of bands, height and width in pixels. Long and short
options are provided for each of these. Coordinate reference system
and affine transformation matrix are not strictly required and have
long options only. All other format specific creation outputs must
be specified using the --co option.
Simple north-up, non-rotated georeferencing can be set by using the
--bounds option. The --transform option will assign an arbitrarily
rotated affine transformation matrix to the dataset. Ground control
points, rational polynomial coefficients, and geolocation matrices
are not supported.
The pixel values of an empty dataset are format specific. "Smart"
formats like GTiff use 0 or the nodata value if provided.
Example:
\b
$ rio create new.tif -f GTiff -t uint8 -n 3 -h 512 -w 512 \\
> --co tiled=true --co blockxsize=256 --co blockysize=256
The command above produces a 3-band GeoTIFF with 256 x 256 internal
tiling.
"""
# Preventing rio create from overwriting local and remote files,
# objects, and datasets is complicated.
if os.path.exists(output):
if not overwrite:
raise FileOverwriteError(
"File exists and won't be overwritten without use of the '--overwrite' option."
)
else: # Check remote or other non-file output.
try:
with rasterio.open(output) as dataset:
# Dataset exists. May or may not be overwritten.
if not overwrite:
raise FileOverwriteError(
"Dataset exists and won't be overwritten without use of the '--overwrite' option."
)
except RasterioIOError as exc:
# TODO: raise a different exception from rasterio.open() in
# this case?
if "No such file or directory" in str(exc):
pass # Good, output does not exist. Continue with no error.
else:
# Remote output exists, but is not a rasterio dataset.
if not overwrite:
raise FileOverwriteError(
"Object exists and won't be overwritten without use of the '--overwrite' option."
)

# Prepare the dataset's georeferencing.
geo_transform = None

if bounds:
left, bottom, right, top = bounds
sx = (right - left) / width
sy = (bottom - top) / height
geo_transform = Affine.translation(left, top) * Affine.scale(sx, sy)
if transform:
if geo_transform is not None:
click.echo(
"--transform value is overriding --bounds value. "
"Use only one of these options to avoid this warning.",
err=True,
)
geo_transform = transform

profile = dict(
driver=driver,
dtype=dtype,
count=count,
height=height,
width=width,
nodata=nodata,
crs=crs,
transform=geo_transform,
**creation_options,
)

with ctx.obj["env"], rasterio.open(output, "w", **profile) as dataset:
pass
13 changes: 8 additions & 5 deletions rasterio/rio/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,13 @@ def bounds_handler(ctx, param, value):
multiple=True,
help="Indexes of input file bands.")

# TODO: may be better suited to cligj
# TODO: may be better suited to cligj?
bounds_opt = click.option(
'--bounds', default=None, callback=bounds_handler,
help='Bounds: "left bottom right top" or "[left, bottom, right, top]".')
"--bounds",
default=None,
callback=bounds_handler,
help="Bounds: 'left bottom right top' or '[left, bottom, right, top]'.",
)

dimensions_opt = click.option(
'--dimensions',
Expand Down Expand Up @@ -370,5 +373,5 @@ def bounds_handler(ctx, param, value):
"feature collection object.")

format_opt = click.option(
'-f', '--format', '--driver', 'driver',
help="Output format driver")
"-f", "--format", "--driver", "driver", help="Output format driver."
)
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ rasterio.rio_commands =
calc = rasterio.rio.calc:calc
clip = rasterio.rio.clip:clip
convert = rasterio.rio.convert:convert
create = rasterio.rio.create:create
edit-info = rasterio.rio.edit_info:edit
env = rasterio.rio.env:env
gcps = rasterio.rio.gcps:gcps
Expand Down

0 comments on commit 35a5906

Please sign in to comment.