# Copyright 2018-2023 Descartes Labs.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
The Scene class holds metadata about a single scene in the Descartes Labs catalog.
Example
-------
>>> import descarteslabs as dl
>>> scene, ctx = dl.scenes.Scene.from_id("landsat:LC08:PRE:TOAR:meta_LC80270312016188_v1") # doctest: +SKIP
>>> ctx # a default GeoContext to use when loading raster data from this Scene # doctest: +SKIP
AOI(geometry=None,
resolution=15.0,
crs='EPSG:32615',
align_pixels=False,
bounds=(258292.5, 4503907.5, 493732.5, 4743307.5),
bounds_crs='EPSG:32615',
shape=None,
all_touched=False)
>>> scene.properties.id # doctest: +SKIP
'landsat:LC08:PRE:TOAR:meta_LC80270312016188_v1'
>>> scene.properties.date # doctest: +SKIP
datetime.datetime(2016, 7, 6, 16, 59, 42, 753476, tzinfo=<UTC>)
>>> scene.properties.bands.red.resolution # doctest: +SKIP
15
>>> arr = scene.ndarray("red green blue", ctx.assign(resolution=120.)) # doctest: +SKIP
>>> type(arr) # doctest: +SKIP
<class 'numpy.ma.core.MaskedArray'>
>>> arr.shape # doctest: +SKIP
(3, 1995, 1962)
"""
from descarteslabs.exceptions import NotFoundError
from ..client.deprecation import deprecate
from ..catalog import Image
from ..common.dotdict import DotDict
from .helpers import REQUEST_PARAMS, cached_bands_by_product
# more or less like a DotDict but delegates everything to the underlying image
# most importantly, it is lazy about retrieving bands
class _PropertiesAccessor(object):
def __init__(self, image):
self.__dict__["_image"] = image
# The following three properties are not part of the metadata model,
# but instead were computed and added to the metadata dict by the old
# Scenes constructor. We implement them via @property here instead
# as all but `bands` are lightweight, and `bands` is best deferred until
# actually needed.
@property
def bands(self):
return DotDict(
cached_bands_by_product(self._image.product_id, self._image._client)
)
@property
def crs(self):
return self._image.cs_code or self._image.projection
@property
def date(self):
return self._image.acquired
def get(self, key, default=None):
try:
return getattr(self, key)
except AttributeError:
return default
def __getitem__(self, key):
try:
return getattr(self, key)
except AttributeError:
raise KeyError(key) from None
def __setitem__(self, key, value):
raise TypeError("Properties are read-only")
def __delitem__(self, key):
raise TypeError("Properties are read-only")
def __getattr__(self, attr):
try:
return DotDict._box(self._image.v1_properties[attr])
except KeyError:
raise AttributeError(attr) from None
def __setattr__(self, attr, value):
raise TypeError("Properties are read-only")
def __delattr__(self, attr):
raise TypeError("Properties are read-only")
def __iter__(self):
for k in self._image.v1_properties.keys():
yield k
yield "date"
yield "crs"
yield "bands"
def __dir__(self):
return super(_PropertiesAccessor, self).__dir__() + list(
self._image.v1_properties.keys()
)
def keys(self):
return self.__iter__()
def values(self):
for k, v in self.items():
yield v
def items(self):
for kv in DotDict(self._image.v1_properties.items()):
yield kv
yield ("date", self.date)
yield ("crs", self.crs)
yield ("bands", self.bands)
[docs]class Scene(object):
"""
Object holding metadata about a single scene in the Descartes Labs catalog.
A Scene is structured like a GeoJSON Feature, with geometry and properties.
Attributes
----------
geometry : shapely.geometry.Polygon
The region the scene's data covers, in WGS84 (lat-lon) coordinates,
represented as a Shapely polygon.
properties : DotDict-like
Metadata about the scene. Some fields will vary between products,
but these will be present:
* ``id`` : str
Descartes Labs ID of this scene
* ``crs`` : str
Native coordinate reference system of the scene,
as an EPSG code or PROJ.4 definition
* ``date`` : datetime.datetime
``'acquired'`` date parsed as a Python datetime if set,
otherwise None
* ``bands`` : DotDict[str, DotDict]
Metadata about the bands available in the scene,
as the mapping ``{band name: band metadata}``.
Band names are either the band's ``name`` field (like "red"),
or for derived bands, the band's ``id`` (like "derived:ndvi").
Each band metadata dict should contain these fields:
* ``id`` : str
Descartes Labs ID of the band;
unique to every band of every product
* ``name`` : str
Human-readable name of the band
* ``dtype`` : str
Native type in which the band's data is stored
* ``data_range`` : list
List of [min, max] values the band's data can have
These fields are useful and available in most products,
but may not always be available:
* ``resolution`` : float
Native resolution of the band, in ``resolution_unit``,
that the edge of each pixel represents on the ground
* ``resolution_unit`` : str
Units of ``resolution`` field, such as ``"m"``
* ``physical_range`` : list
[min, max] range of values the band's data *represents*.
Values of data have physical meaning
(such as a reflectance fraction from 0-1), but often
those values are remapped to a different numerical range
for more efficient storage (since fixed-point integers require
less space than floats). To return data to numbers
with physical meaning, they should be mapped
from ``data_range`` to ``physical_range``.
* ``wavelength_min``
Minimum wavelength captured by the sensor in this band
* ``wavelength_center``
Central wavelength captured by the sensor in this band
* ``wavelength_max``
Maximum wavelength captured by the sensor in this band
* ``wavelength_unit``
Units of the wavelength fields, such as ``"nm"``
"""
def __init__(self, image: Image):
"""
``__init__`` instantiates a Scene by wrapping a `descarteslabs.catalog.Image` instead.
It's preferred to use `Scene.from_id` or `scenes.search <scenes.search_api.search>` instead.
"""
if not isinstance(image, Image):
ValueError("image must be a descarteslabs.catalog.Image")
self._image = image
@property
def geometry(self):
return self._image.geometry
@property
def properties(self):
return _PropertiesAccessor(self._image)
@property
def __geo_interface__(self):
return self.geometry.__geo_interface__
[docs] @classmethod
@deprecate(removed=["metadata_client"])
def from_id(cls, scene_id):
"""
Return the metadata for a Descartes Labs scene ID as a Scene object.
Also returns a :class:`~descarteslabs.common.geo.geocontext.GeoContext`
for loading the Scene's original, unwarped data.
Parameters
----------
scene_id: str
Descartes Labs scene ID,
e.g. "landsat:LC08:PRE:TOAR:meta_LC80270312016188_v1"
Returns
-------
scene: Scene
Scene instance with metadata loaded from the Descartes Labs catalog
ctx: AOI
A :class:`~descarteslabs.common.geo.geocontext.GeoContext` for loading this Scene's original data.
The defaults used are described in `Scene.default_ctx`.
Example
-------
>>> import descarteslabs as dl
>>> scene, ctx = dl.scenes.Scene.from_id("landsat:LC08:PRE:TOAR:meta_LC80260322016197_v1") # doctest: +SKIP
>>> ctx # doctest: +SKIP
AOI(geometry=None,
resolution=15.0,
crs='EPSG:32615',
align_pixels=False,
bounds=(348592.5, 4345567.5, 581632.5, 4582807.5),
bounds_crs='EPSG:32615',
shape=None.
all_touched=True)
>>> scene.properties.date # doctest: +SKIP
datetime.datetime(2016, 7, 15, 16, 53, 59, 495435, tzinfo=<UTC>)
Raises
------
NotFoundError
If the ``scene_id`` cannot be found in the Descartes Labs catalog
"""
image = Image.get(scene_id, request_params=REQUEST_PARAMS)
if image is None:
raise NotFoundError("Scene {scene_id} not found")
return cls(image), image.geocontext
[docs] def default_ctx(self):
"""
Return an :class:`AOI GeoContext <descarteslabs.common.geo.geocontext.AOI>`
for loading this Scene's original, unwarped data.
These defaults are used:
* resolution: resolution determined from the Scene's ``geotrans``
* crs: native CRS of the Scene (often, a UTM CRS)
* bounds: bounds determined from the Scene's ``geotrans`` and ``raster_size``
* bounds_crs: native CRS of the Scene
* align_pixels: False, to prevent interpolation snapping pixels to a new grid
* geometry: None
.. note::
Using this :class:`~descarteslabs.common.geo.geocontext.GeoContext` will only
return original, unwarped data if the Scene is axis-aligned ("north-up")
within the CRS. If its ``geotrans`` applies a rotation, a warning will be raised.
In that case, use `Raster.ndarray` or `Raster.raster` to retrieve
original data. (The :class:`~descarteslabs.common.geo.geocontext.GeoContext`
paradigm requires bounds for consistentcy, which are inherently axis-aligned.)
Returns
-------
ctx: AOI
"""
return self._image.geocontext
[docs] def coverage(self, geom):
"""
The fraction of a geometry-like object covered by this Scene's geometry.
Parameters
----------
geom : GeoJSON-like dict, :class:`~descarteslabs.common.geo.geocontext.GeoContext`, or object with __geo_interface__ # noqa: E501
Geometry to which to compare this Scene's geometry
Returns
-------
coverage: float
The fraction of ``geom``'s area that overlaps with this Scene,
between 0 and 1.
Example
-------
>>> import descarteslabs as dl
>>> scene, ctx = dl.scenes.Scene.from_id("landsat:LC08:PRE:TOAR:meta_LC80270312016188_v1") # doctest: +SKIP
>>> scene.coverage(scene.geometry.buffer(1)) # doctest: +SKIP
0.258370644415335
"""
return self._image.coverage(geom)
[docs] @deprecate(removed=["raster_client"])
def ndarray(
self,
bands,
ctx,
mask_nodata=True,
mask_alpha=None,
bands_axis=0,
raster_info=False,
resampler="near",
processing_level=None,
scaling=None,
data_type=None,
progress=None,
):
"""
Load bands from this scene as an ndarray, optionally masking invalid data.
If the selected bands have different data types the resulting ndarray
has the most general of those data types. This table defines which data types
can be cast to which more general data types:
* ``Byte`` to: ``UInt16``, ``UInt32``, ``Int16``, ``Int32``, ``Float32``, ``Float64``
* ``UInt16`` to: ``UInt32``, ``Int32``, ``Float32``, ``Float64``
* ``UInt32`` to: ``Float64``
* ``Int16`` to: ``Int32``, ``Float32``, ``Float64``
* ``Int32`` to: ``Float32``, ``Float64``
* ``Float32`` to: ``Float64``
* ``Float64`` to: No possible casts
Parameters
----------
bands : str or Sequence[str]
Band names to load. Can be a single string of band names
separated by spaces (``"red green blue derived:ndvi"``),
or a sequence of band names (``["red", "green", "blue", "derived:ndvi"]``).
Names must be keys in ``self.properties.bands``.
If the alpha band is requested, it must be last in the list
to reduce rasterization errors.
ctx : :class:`~descarteslabs.common.geo.geocontext.GeoContext`
A :class:`~descarteslabs.common.geo.geocontext.GeoContext` to use when loading this Scene
mask_nodata : bool, default True
Whether to mask out values in each band that equal
that band's ``nodata`` sentinel value.
mask_alpha : bool or str or None, default None
Whether to mask pixels in all bands where the alpha band of the scene is 0.
Provide a string to use an alternate band name for masking.
If the alpha band is available and ``mask_alpha`` is None, ``mask_alpha``
is set to True. If not, mask_alpha is set to False.
bands_axis : int, default 0
Axis along which bands should be located in the returned array.
If 0, the array will have shape ``(band, y, x)``, if -1,
it will have shape ``(y, x, band)``.
It's usually easier to work with bands as the outermost axis,
but when working with large arrays, or with many arrays concatenated
together, NumPy operations aggregating each xy point across bands
can be slightly faster with bands as the innermost axis.
raster_info : bool, default False
Whether to also return a dict of information about the rasterization
of the scene, including the coordinate system WKT and geotransform matrix.
Generally only useful if you plan to upload data derived
from this scene back to the Descartes catalog, or use it with GDAL.
resampler : str, default "near"
Algorithm used to interpolate pixel values when scaling and transforming
the image to its new resolution or CRS. Possible values are
``near`` (nearest-neighbor), ``bilinear``, ``cubic``, ``cubicsplice``,
``lanczos``, ``average``, ``mode``, ``max``, ``min``, ``med``, ``q1``, ``q3``.
processing_level : str, optional
How the processing level of the underlying data should be adjusted. Possible
values depend on the product and bands in use. Legacy products support
``toa`` (top of atmosphere) and in some cases ``surface``. Consult the
available ``processing_levels`` in the product bands to understand what
is available.
scaling : None, str, list, dict
Band scaling specification. Please see :meth:`scaling_parameters` for a full
description of this parameter.
data_type : None, str
Output data type. Please see :meth:`scaling_parameters` for a full
description of this parameter.
progress : None, bool
Controls display of a progress bar.
raster_client : Raster, optional
Unneeded in general use; lets you use a specific client instance
with non-default auth and parameters.
Returns
-------
arr : ndarray
Returned array's shape will be ``(band, y, x)`` if bands_axis is 0,
``(y, x, band)`` if bands_axis is -1.
If ``mask_nodata`` or ``mask_alpha`` is True, arr will be a masked array.
The data type ("dtype") of the array is the most general of the data
types among the bands being rastered.
raster_info : dict
If ``raster_info=True``, a raster information dict is also returned.
Example
-------
>>> import descarteslabs as dl
>>> scene, ctx = dl.scenes.Scene.from_id("landsat:LC08:PRE:TOAR:meta_LC80270312016188_v1") # doctest: +SKIP
>>> arr = scene.ndarray("red green blue", ctx.assign(resolution=120.)) # doctest: +SKIP
>>> type(arr) # doctest: +SKIP
<class 'numpy.ma.core.MaskedArray'>
>>> arr.shape # doctest: +SKIP
(3, 1995, 1962)
>>> red_band = arr[0] # doctest: +SKIP
Raises
------
ValueError
If requested bands are unavailable.
If band names are not given or are invalid.
If the requested bands have incompatible dtypes.
NotFoundError
If a Scene's ID cannot be found in the Descartes Labs catalog
BadRequestError
If the Descartes Labs Platform is given invalid parameters
"""
return self._image.ndarray(
bands,
geocontext=ctx,
mask_nodata=mask_nodata,
mask_alpha=mask_alpha,
bands_axis=bands_axis,
raster_info=raster_info,
resampler=resampler,
processing_level=processing_level,
scaling=scaling,
data_type=data_type,
progress=progress,
)
def has_alpha(self, alpha_band_name):
return alpha_band_name in self.properties.bands
[docs] @deprecate(removed=["raster_client"])
def download(
self,
bands,
ctx,
dest=None,
format="tif",
resampler="near",
processing_level=None,
scaling=None,
data_type=None,
nodata=None,
progress=None,
):
"""
Save bands from this scene as a GeoTIFF, PNG, or JPEG, writing to a path.
Parameters
----------
bands : str or Sequence[str]
Band names to load. Can be a single string of band names
separated by spaces (``"red green blue derived:ndvi"``),
or a sequence of band names (``["red", "green", "blue", "derived:ndvi"]``).
Names must be keys in ``self.properties.bands``.
ctx : :class:`~descarteslabs.common.geo.geocontext.GeoContext`
A :class:`~descarteslabs.common.geo.geocontext.GeoContext` to use when loading this Scene
dest : str or path-like object, default None
Where to write the image file.
* If None (default), it's written to an image file of the given ``format``
in the current directory, named by the Scene's ID and requested bands,
like ``"sentinel-2:L1C:2018-08-10_10TGK_68_S2A_v1-red-green-blue.tif"``
* If a string or path-like object, it's written to that path.
Any file already existing at that path will be overwritten.
Any intermediate directories will be created if they don't exist.
Note that path-like objects (such as pathlib.Path) are only supported
in Python 3.6 or later.
format : str, default "tif"
If None is given as ``dest``: one of "tif", "png", or "jpg".
If a str or path-like object is given as ``dest``, ``format`` is ignored
and determined from the extension on the path (one of ".tif", ".png", or ".jpg").
resampler : str, default "near"
Algorithm used to interpolate pixel values when scaling and transforming
the image to its new resolution or SRS. Possible values are
``near`` (nearest-neighbor), ``bilinear``, ``cubic``, ``cubicsplice``,
``lanczos``, ``average``, ``mode``, ``max``, ``min``, ``med``, ``q1``, ``q3``.
processing_level : str, optional
How the processing level of the underlying data should be adjusted. Possible
values depend on the product and bands in use. Legacy products support
``toa`` (top of atmosphere) and in some cases ``surface``. Consult the
available ``processing_levels`` in the product bands to understand what
is available.
scaling : None, str, list, dict
Band scaling specification. Please see :meth:`scaling_parameters` for a full
description of this parameter.
data_type : None, str
Output data type. Please see :meth:`scaling_parameters` for a full
description of this parameter.
nodata : None, number
NODATA value for a geotiff file. Will be assigned to any masked pixels.
progress : None, bool
Controls display of a progress bar.
Returns
-------
path : str or None
If ``dest`` is None or a path, the path where the image file was written is returned.
If ``dest`` is file-like, nothing is returned.
Example
-------
>>> import descarteslabs as dl
>>> scene, ctx = dl.scenes.Scene.from_id("landsat:LC08:PRE:TOAR:meta_LC80270312016188_v1") # doctest: +SKIP
>>> scene.download("red green blue", ctx.assign(resolution=120.)) # doctest: +SKIP
"landsat:LC08:PRE:TOAR:meta_LC80270312016188_v1_red-green-blue.tif"
>>> import os
>>> os.listdir(".") # doctest: +SKIP
["landsat:LC08:PRE:TOAR:meta_LC80270312016188_v1-red-green-blue.tif"]
>>> scene.download(
... "red green blue",
... ctx,
... "rasters/{ctx.resolution}-{scene.properties.id}.jpg".format(ctx=ctx, scene=scene)
... ) # doctest: +SKIP
"rasters/15-landsat:LC08:PRE:TOAR:meta_LC80270312016188_v1.tif"
Raises
------
ValueError
If requested bands are unavailable.
If band names are not given or are invalid.
If the requested bands have incompatible dtypes.
If ``format`` is invalid, or the path has an invalid extension.
NotFoundError
If a Scene's ID cannot be found in the Descartes Labs catalog
BadRequestError
If the Descartes Labs Platform is given invalid parameters
"""
return self._image.download(
bands,
geocontext=ctx,
dest=dest,
format=format,
resampler=resampler,
processing_level=processing_level,
scaling=scaling,
data_type=data_type,
nodata=nodata,
progress=progress,
)
[docs] def scaling_parameters(
self, bands, processing_level=None, scaling=None, data_type=None
):
"""
Computes fully defaulted scaling parameters and output data_type
from provided specifications.
This method makes accessible the scales and data_type parameters
which will be generated and passed to the Raster API by methods
such as :meth:`ndarray` and :meth:`download`. It is provided
as a convenience to the user to aid in understanding how the
``scaling`` and ``data_type`` parameters will be handled by
those methods. It would not usually be used in a normal workflow.
Parameters
----------
bands : list
List of bands to be scaled.
processing_level : str, optional
How the processing level of the underlying data should be adjusted. Possible
values depend on the product and bands in use. Legacy products support
``toa`` (top of atmosphere) and in some cases ``surface``. Consult the
available ``processing_levels`` in the product bands to understand what
is available.
scaling : None or str or list or dict, default None
Supplied scaling specification, see below.
data_type : None or str, default None
Result data type desired, as a standard data type string (e.g.
``"Byte"``, ``"Uint16"``, or ``"Float64"``). If not specified,
will be deduced from the ``scaling`` specification. Typically
this is left unset and the appropriate type will be determined
automatically.
Returns
-------
scales : list(tuple)
The fully specified scaling parameter, compatible with the
:class:`~descarteslabs.client.services.raster.Raster` API and the
output data type.
data_type : str
The result data type as a standard GDAL type string.
Raises
------
ValueError
If any invalid or incompatible value is passed to any of the
three parameters.
Scaling is determined on a band-by-band basis, incorporating the user
provided specification, the output data_type, and properties for the
band, such as the band type, the band data type, and the
``default_range``, ``data_range``, and ``physical_range`` properties.
Ultimately the scaling for each band will be resolved to either
``None`` or a tuple of numeric values of length 0, 2, or 4, as
accepted by the Raster API. The result is a list (with length equal
to the number of bands) of one of these values, or may be a None
value which is just a shorthand equivalent for a list of None values.
A ``None`` indicates that no scaling should be performed.
A 0-tuple ``()`` indicates that the band data should be automatically
scaled from the minimum and maximum values present in the image data
to the display range 0-255.
A 2-tuple ``(input-min, input-max)`` indicates that the band data
should be scaled from the specified input range to the display
range of 0-255.
A 4-tuple ``(input-min, input-max, output-min, output-max)``
indicates that the band data should be scaled from the input range
to the output range.
In all cases, the scaling will be performed as a multiply and add,
and the resulting values are only clipped as necessary to fit in
the output data type. As such, if the input and output ranges are
the same, it is effectively a no-op equivalent to ``None``.
The support for scaling parameters in the ``Scenes`` API includes
the concept of an automated scaling mode. The four supported modes
are as follows.
``"raw"``:
Equivalent to a ``None``, the data should not be scaled.
``"auto"``:
Equivalent to a 0-tuple, the data should be scaled by
the Raster service so that the actual range of data in the
input is scaled up to the full display range (0-255). It
is not possible to determine the bounds of this input range
in the Scenes API as the actual band data is not accessible.
``"display"``:
The data should be scaled from any specified input bounds,
defaulting to the ``default_range`` property for the band,
to the output range, defaulting to 0-255.
``"physical"``:
The data should be scaled from the input range, defaulting
to the ``data_range`` property for the band, to the output
range, defaulting to the ``physical_range`` property for
the band.
The mode may be explicitly specified, or it may be determined
implicitly from other characteristics such as the length
and contents of the tuples for each band, or from the output
data_type if this is explicitly specified (e.g. ``"Byte"``
implies display mode, ``"Float64"`` implies physical mode).
If it is not possible to infer the mode, and a mode is required
in order to fully determine the results of this method, an
error will be raised. It is also an error to explicitly
specify more than one mode, with several exceptions: auto
and display mode are compatible, while a raw display mode
for a band which is of type "mask" or type "class" does
not conflict with any other mode specification.
Normally the ``data_type`` parameter is not provided by the
user, and is instead determined from the mode as follows.
``"raw"``:
The data type that best matches the data types of all
the bands, preserving the precision and range of the
original data.
``"auto"`` and ``"display"``:
``"Byte"``
``"physical"``:
``"Float64"``
The ``scaling`` parameter passed to this method can be any
of the following:
None:
No scaling for all bands. Equivalent to ``[None, ...]``.
str:
Any of the four supported automatic modes as
described above.
list or Iterable:
A list or similar iterable must contain a number of
elements equal to the number of bands specified. Each
element must either be a None, a 0-, 2-, or 4-tuple, or
one of the above four automatic mode strings. The
elements of each tuple must either be a numeric value
or a string containing a valid numerical string followed
by a "%" character. The latter will be interpreted as a
percentage of the appropriate range (e.g. ``default_range``,
``data_range``, or ``physical_range``) according to the mode.
For example, a tuple of ``("25%", "75%")`` with a
``default_range`` of ``[0, 4000]`` will yield ``(1000, 3000)``.
dict or Mapping:
A dictionary or similar mapping with keys corresponding to
band names and values as accepted as elements for each band
as with a list described above. Each band name is used to
lookup a value in the mapping. If none is found, and the
band is not of type "mask" or "class", then the special
key ``"default_"`` is looked up in the mapping if it exists.
Otherwise a value of ``None`` will be used for the band.
This is strictly a convenience for constructing a list of
scale values, one for each band, but can be useful if a
single general-purpose mapping is defined for all possible
or relevant bands and then reused across many calls to the
different methods in the Scenes API which accept a ``scaling``
parameter.
See Also
--------
:doc:`Scenes Guide </guides/scenes>` : This contains many examples of the use of
the ``scaling`` and ``data_type`` parameters.
"""
return self._image.scaling_parameters(
bands, processing_level, scaling, data_type
)
# not sure this is even needed?
# def _dict(self):
# return dict(geometry=self.__geo_interface__, properties=DotDict({k: v for k, v in self.properties})
def __repr__(self):
parts = [
'Scene "{}"'.format(self.properties.get("id")),
' * Product: "{}"'.format(self.properties.get("product")),
' * CRS: "{}"'.format(self.properties.get("crs")),
]
try:
date = " * Date: {:%c}".format(self.properties.get("date"))
parts.append(date)
except Exception:
pass
bands = self.properties.get("bands")
if bands is not None:
if len(bands) > 30:
parts += [" * Bands: {}".format(len(bands))]
else:
# strings will be formatted with a band dict as available fields
part_format_strings = [
"{resolution}",
"{resolution_unit},",
"{dtype},",
"{data_range}",
"-> {physical_range}",
'in units "{data_unit}"',
]
band_lines = []
# QUESTION(gabe): should there be a canonical ordering to bands? (see GH #973)
for bandname, band in bands.items():
band_line = " * " + bandname
band_parts = []
for format_string in part_format_strings:
try:
# If the named field in `format_string` is missing from `band`,
# `format_string.format(**band)` will fail with a KeyError, which we catch.
band_parts.append(format_string.format(**band))
except (KeyError, ValueError):
pass
if len(band_parts) > 0:
band_line = band_line + ": " + " ".join(band_parts)
band_lines.append(band_line)
if len(band_lines) > 0:
parts += [" * Bands:"] + band_lines
return "\n".join(parts)