diff --git a/README.md b/README.md index 6ab637b..69c2d17 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Intake-stac +# Intake-STAC ![CI](https://github.com/pangeo-data/intake-stac/workflows/CI/badge.svg) [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/pangeo-data/intake-stac/master?filepath=examples?urlpath=lab) @@ -6,37 +6,35 @@ [![Documentation Status](https://readthedocs.org/projects/intake-stac/badge/?version=latest)](https://intake-stac.readthedocs.io/en/latest/?badge=latest) [![codecov](https://codecov.io/gh/pangeo-data/intake-stac/branch/master/graph/badge.svg)](https://codecov.io/gh/pangeo-data/intake-stac) -This is an [intake](https://intake.readthedocs.io/en/latest) data source for [SpatioTemporal Asset Catalogs (STAC)](https://stacspec.org/). The STAC specification provides a common metadata specification, API, and catalog format to describe geospatial assets, so they can more easily indexed and discovered. A 'spatiotemporal asset' is any file that represents information about the earth captured in a certain space and time. +This is an [Intake](https://intake.readthedocs.io/en/latest) data source for [SpatioTemporal Asset Catalogs (STAC)](https://stacspec.org/). The STAC specification provides a common metadata specification, API, and catalog format to describe geospatial assets, so they can more easily indexed and discovered. A 'spatiotemporal asset' is any file that represents information about the earth captured in a certain space and time. -Two examples of STAC catalogs are: - -- https://planet.stac.cloud/?t=catalogs -- https://landsat-stac.s3.amazonaws.com/catalog.json - -[Radiant Earth](https://radiant.earth) keeps track of a more complete listing of STAC implementations [here](https://github.com/radiantearth/stac-spec/blob/master/implementations.md). - -This project provides an opinionated way for users to load datasets from these catalogs into the scientific Python ecosystem. It uses the intake-xarray plugin and supports several file formats including GeoTIFF, netCDF, GRIB, and OpenDAP. +Intake-STAC provides an opinionated way for users to load Assets from STAC catalogs into the scientific Python ecosystem. It uses the [intake-xarray](https://github.com/intake/intake-xarray) plugin and supports several file formats including GeoTIFF, netCDF, GRIB, and OpenDAP. ## Installation -intake-stac has a few [requirements](requirements.txt), such as [Intake](https://intake.readthedocs.io), [intake-xarray](https://intake-xarray.readthedocs.io/) and [sat-stac](https://github.com/sat-utils/sat-stac). Intake-stac can be installed in any of the following ways: +Intake-STAC has a few [requirements](requirements.txt), such as [Intake](https://intake.readthedocs.io), [intake-xarray](https://intake-xarray.readthedocs.io/) and [sat-stac](https://github.com/sat-utils/sat-stac). Intake-stac can be installed in any of the following ways: -Using conda: +We recommend installing the latest release with `conda`: ```bash $ conda install -c conda-forge intake-stac ``` -Using Pip: +Or the latest development version with `pip`: ```bash -$ pip install intake-stac +$ pip install git+https://github.com/intake/intake-stac ``` -Or from the source repository: +## Examples -```bash -$ pip install git+https://github.com/pangeo-data/intake-stac +```python +from intake import open_stac_catalog +catalog_url = 'https://raw.githubusercontent.com/cholmes/sample-stac/master/stac/catalog.json' +cat = open_stac_catalog(catalog_url) +cat['Houston-East-20170831-103f-100d-0f4f-RGB'].metadata +da = cat['Houston-East-20170831-103f-100d-0f4f-RGB']['thumbnail'].to_dask() +da ``` The [examples/](examples/) directory contains some example Jupyter Notebooks that can be used to test the functionality. @@ -51,18 +49,10 @@ pip install intake-stac==0.1.0 The table below shows the corresponding versions between intake-stac and STAC: -| sat-stac | STAC | -| --------- | ----- | -| 0.[1,2].x | 0.6.x | - -### Running the tests - -To run the tests some additional packages need to be installed from the `requirements-dev.txt` file. - -``` -$ pip install -r requirements-dev.txt -$ pytest -v -s --cov intake-stac --cov-report term-missing -``` +| intake-stac | STAC | +| ----------- | ----- | +| 0.2.x | 0.6.x | +| 0.3.x | 1.0.x | ## About diff --git a/ci/environment-dev-3.6.yml b/ci/environment-dev-3.6.yml index 1cc201b..e06adc1 100644 --- a/ci/environment-dev-3.6.yml +++ b/ci/environment-dev-3.6.yml @@ -2,6 +2,7 @@ name: intake-stac-dev channels: - conda-forge dependencies: + - aiohttp - autopep8 - black - boto3 @@ -10,9 +11,13 @@ dependencies: - dask - distributed - flake8 + - geopandas - intake + - intake-geopandas + - intake-parquet - intake-xarray - ipykernel + - ipywidgets - isort - make - nbsphinx @@ -28,17 +33,14 @@ dependencies: - python=3.6 - pytoml - pyyaml + - rasterio - recommonmark - - sat-search + - requests - sat-stac - scikit-image - sphinx_rtd_theme - sphinx>=1.6 - xarray - - rasterio - - ipywidgets - - geopandas - - intake-geopandas - - intake-parquet - pip: + - sat-search==0.3.0rc1 - sphinx_copybutton diff --git a/ci/environment-dev-3.7-upstream.yml b/ci/environment-dev-3.7-upstream.yml index 7ee6c39..183cdf3 100644 --- a/ci/environment-dev-3.7-upstream.yml +++ b/ci/environment-dev-3.7-upstream.yml @@ -2,6 +2,7 @@ name: intake-stac-dev channels: - conda-forge dependencies: + - aiohttp - autopep8 - black - boto3 @@ -10,7 +11,9 @@ dependencies: - dask - distributed - flake8 + - geopandas - ipykernel + - ipywidgets - isort - make - nbsphinx @@ -26,14 +29,13 @@ dependencies: - python=3.7 - pytoml - pyyaml + - rasterio - recommonmark + - requests - scikit-image - sphinx_rtd_theme - sphinx>=1.6 - xarray - - rasterio - - ipywidgets - - geopandas - pip: - sphinx_copybutton - git+https://github.com/sat-utils/sat-stac.git diff --git a/ci/environment-dev-3.7.yml b/ci/environment-dev-3.7.yml index af30199..66afb29 100644 --- a/ci/environment-dev-3.7.yml +++ b/ci/environment-dev-3.7.yml @@ -2,6 +2,7 @@ name: intake-stac-dev channels: - conda-forge dependencies: + - aiohttp - autopep8 - black - boto3 @@ -10,9 +11,13 @@ dependencies: - dask - distributed - flake8 + - geopandas - intake + - intake-geopandas + - intake-parquet - intake-xarray - ipykernel + - ipywidgets - isort - make - nbsphinx @@ -28,17 +33,14 @@ dependencies: - python=3.7 - pytoml - pyyaml + - rasterio - recommonmark - - sat-search + - requests - sat-stac - scikit-image - sphinx_rtd_theme - sphinx>=1.6 - xarray - - rasterio - - ipywidgets - - geopandas - - intake-geopandas - - intake-parquet - pip: + - sat-search==0.3.0rc1 - sphinx_copybutton diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst index f10e561..f22f07d 100644 --- a/docs/source/contributing.rst +++ b/docs/source/contributing.rst @@ -17,7 +17,7 @@ Feature requests and feedback Do you like intake-stac? Share some love on Twitter or in your blog posts! We'd also like to hear about your propositions and suggestions. Feel free to -`submit them as issues `_ and: +`submit them as issues `_ and: * Explain in detail how they should work. * Keep the scope as narrow as possible. This will make it easier to implement. @@ -28,7 +28,7 @@ We'd also like to hear about your propositions and suggestions. Feel free to Report bugs ----------- -Report bugs for intake-stac in the `issue tracker `_. +Report bugs for intake-stac in the `issue tracker `_. If you are reporting a bug, please include: @@ -48,7 +48,7 @@ fix the bug itself. Fix bugs -------- -Look through the `GitHub issues for bugs `_. +Look through the `GitHub issues for bugs `_. Talk to developers to find out how you can fix specific bugs. @@ -70,7 +70,7 @@ without using a local copy. This can be convenient for small fixes. .. code:: bash - $ conda env update -f ci/environment-dev-3.7.yml + $ conda env create -f ci/environment-dev-3.7.yml $ cd docs $ make html @@ -86,10 +86,11 @@ Preparing Pull Requests #. Fork the - `intake-stac GitHub repository `__. It's + `intake-stac GitHub repository `__. It's fine to use ``intake-stac`` as your fork repository name because it will live under your user. + #. Clone your fork locally using `git `_ and create a branch:: $ git clone git@github.com:YOUR_GITHUB_USERNAME/intake-stac.git @@ -100,6 +101,13 @@ Preparing Pull Requests $ git checkout -b your-bugfix-feature-branch-name master +#. Install development version in a conda environment:: + + $ conda env create -f ci/environment-dev-3.7.yml + $ conda activate intake-stac-dev + $ pip install . -e + + #. Install `pre-commit `_ and its hook on the intake-stac repo:: $ pip install --user pre-commit @@ -109,25 +117,23 @@ Preparing Pull Requests https://pre-commit.com/ is a framework for managing and maintaining multi-language pre-commit hooks to ensure code-style and code formatting is consistent. -#. Install dependencies into a new conda environment:: - - $ conda env update -f ci/environment-dev-3.7.yml #. Run all the tests Now running tests is as simple as issuing this command:: - $ conda activate intake-stac-dev $ pytest --junitxml=test-reports/junit.xml --cov=./ --verbose This command will run tests via the "pytest" tool against Python 3.7. + #. You can now edit your local working copy and run the tests again as necessary. Please follow PEP-8 for naming. When committing, ``pre-commit`` will re-format the files if necessary. + #. Commit and push once your tests pass and you are happy with your change(s):: $ git commit -a -m "" @@ -139,5 +145,5 @@ Preparing Pull Requests head-fork: YOUR_GITHUB_USERNAME/intake-stac compare: your-branch-name - base-fork: pangeo-data/intake-stac + base-fork: intake/intake-stac base: master diff --git a/examples/aws-earth-search.ipynb b/examples/aws-earth-search.ipynb new file mode 100644 index 0000000..4d86712 --- /dev/null +++ b/examples/aws-earth-search.ipynb @@ -0,0 +1,222 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Intake-STAC + sat-search\n", + "================\n", + "\n", + "Commonly, we'll use an API to search a large STAC catalog to narrow down the data we want to work with. Here we'll demonstrate using the [sat-search](https://github.com/sat-utils/sat-search) library that uses https://www.element84.com/earth-search/." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "import intake\n", + "import satsearch\n", + "import os" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bbox = [35.48, -3.24, 35.58, -3.14] # (min lon, min lat, max lon, max lat)\n", + "dates = '2020-07-01/2020-08-15'\n", + "\n", + "os.environ['STAC_API_URL'] = 'https://earth-search.aws.element84.com/v0' # \"stac_version\":\"1.0.0-beta.2\"\n", + "results = satsearch.Search.search(collections=['sentinel-s2-l2a-cogs'], # note collection='sentinel-s2-l2a-cogs' doesn't work\n", + " datetime=dates,\n", + " bbox=bbox, \n", + " sort=[''B5', 'red'->'B4'\n", + "# An alternative organization is to store as a DataSet with common names:\n", + "da['band'] = bands\n", + "ds = da.to_dataset(dim='band')\n", + "ds" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Now we can calculate band indices of subregions easily\n", + "NDVI = (ds['nir'] - ds['red']) / (ds['nir'] + ds['red'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "NDVI.isel(y=slice(2000,3000), x=slice(1500,2000)).plot.imshow(cmap='BrBG', vmin=-1, vmax=1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.8" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/examples/planet-disaster-data.ipynb b/examples/planet-disaster-data.ipynb index 96c5ee0..a01b9e4 100644 --- a/examples/planet-disaster-data.ipynb +++ b/examples/planet-disaster-data.ipynb @@ -4,10 +4,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Examples using STAC with Intake\n", + "Intake-STAC + Planetscope\n", "================\n", "\n", - "In this notebook, we'll take a look at some of the functionality in Intake-STAC by exploring the [Planet Disaster data catalog](https://planet.stac.cloud/?t=catalogs)." + "In this notebook, we'll take a look at some of the functionality in Intake-STAC by exploring STAC catalogs such as the open-access [Planet Disaster data catalog](https://planet.stac.cloud/?t=catalogs) hosted at https://raw.githubusercontent.com/cholmes/sample-stac/master/stac/catalog.json.\n", + "\n", + "STAC metadata is organized into a hierarchy of **Catalogs, Collections, and Items**. Items ultimately hold *Assets* which are data files such as satellite raster images. Ultimately the goal of intake-STAC is to facilitate loading these *Assets* directly into Python objects for interactive computation without worrying about filepaths and URLs." ] }, { @@ -16,8 +18,8 @@ "metadata": {}, "outputs": [], "source": [ - "import intake\n", - "list(intake.registry)" + "%matplotlib inline\n", + "import intake" ] }, { @@ -26,10 +28,9 @@ "metadata": {}, "outputs": [], "source": [ - "%matplotlib inline\n", - "\n", - "from pprint import pprint\n", - "from intake import open_stac_catalog" + "# intake checks for registered drivers when imported\n", + "# You should see 'stac_catalog, stac_collection, stac_item, and stac_item_collection' if intake-stac is installed\n", + "list(intake.registry)" ] }, { @@ -38,7 +39,7 @@ "source": [ "## Catalog adapter\n", "\n", - "We'll start by connecting to the STAC catalog for `planet-disaster-data`. This catalog includes 5 sub-catalogs (know as Items in STAC language)" + "We'll start by connecting to the STAC Catalog for `planet-disaster-data`. We read the top-level `catalog.json` file and drill down until we get to specific STAC Items." ] }, { @@ -47,15 +48,13 @@ "metadata": {}, "outputs": [], "source": [ - "cat = open_stac_catalog('https://storage.googleapis.com/pdd-stac/disasters/catalog.json')\n", - "display(list(cat))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Catalogs include the STAC metadata:" + "# Load root catalog\n", + "url = 'https://raw.githubusercontent.com/cholmes/sample-stac/master/stac/catalog.json'\n", + "cat = intake.open_stac_catalog(url)\n", + "print(cat.name)\n", + "\n", + "# This lists available subcatalogs:\n", + "list(cat)" ] }, { @@ -64,14 +63,8 @@ "metadata": {}, "outputs": [], "source": [ - "pprint(cat.metadata)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "and the metadata from each Item is available:" + "# STAC files are in JSON format, which is accessible as Python dictionaries:\n", + "cat.metadata" ] }, { @@ -80,14 +73,9 @@ "metadata": {}, "outputs": [], "source": [ - "cat['Houston-East-20170831-103f-100d-0f4f-RGB'].metadata" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The sub-catalogs can include an arbitrary number of assets:" + "# Drill down into subcatalogs\n", + "subcat = cat['hurricane-harvey']\n", + "list(subcat)" ] }, { @@ -96,27 +84,9 @@ "metadata": {}, "outputs": [], "source": [ - "list(cat['Houston-East-20170831-103f-100d-0f4f-RGB'])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## GeoTIFF --> `xarray.DataArray`\n", - "\n", - "Using the intake-xarray plugin and xarray's rasterio integration, we can open GeoTIFF datasets remotely. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "tif = cat['Houston-East-20170831-103f-100d-0f4f-RGB']['thumbnail']\n", - "tiff_da = tif.to_dask()\n", - "tiff_da" + "# Another subcatalog!\n", + "subcat1 = subcat['hurricane-harvey-0831']\n", + "list(subcat1)" ] }, { @@ -125,16 +95,8 @@ "metadata": {}, "outputs": [], "source": [ - "tiff_da.plot.imshow(rgb='channel')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Landsat Example\n", - "\n", - "A second example using the landsat archive. This is a much larger catalog than the Planet disasters so we'll skip ahead to a specific row/path. You could also point to the top level landsat-stac catalog but loading sub-catalogs will be pretty slow. We'll be looking into ways of optimizing this going forward.\n" + "# Load a STAC Item\n", + "item = subcat1['Houston-East-20170831-103f-100d-0f4f-RGB']" ] }, { @@ -143,7 +105,7 @@ "metadata": {}, "outputs": [], "source": [ - "landsat = open_stac_catalog('https://landsat-stac.s3.amazonaws.com/landsat-8-l1/011/031/catalog.json')" + "item.metadata" ] }, { @@ -152,7 +114,8 @@ "metadata": {}, "outputs": [], "source": [ - "list(landsat)[:10]" + "# Item Assets are accessible via lists just like subcatalogs:\n", + "list(item)" ] }, { @@ -161,7 +124,7 @@ "metadata": {}, "outputs": [], "source": [ - "list(landsat['LC80110312014230LGN00'])" + "item['thumbnail'].metadata" ] }, { @@ -170,9 +133,9 @@ "metadata": {}, "outputs": [], "source": [ - "tif = landsat['LC80110312014230LGN00.thumbnail']\n", - "tiff_da = tif.to_dask()\n", - "tiff_da" + "# Finally we can display an image!\n", + "from IPython.display import Image\n", + "Image(item['thumbnail'].urlpath)" ] }, { @@ -181,15 +144,11 @@ "metadata": {}, "outputs": [], "source": [ - "tiff_da.isel(x=slice(0, 600), y=slice(300, 900)).plot.imshow(rgb='channel')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Stacking bands\n", - "Sometimes it is helpful to be stack the bands to make it easier to slice all the bands or layer them on top of one another." + "# Or we can load the image directly into Xarray for analysis\n", + "# The full resolution image is big, but we use dask behind the scenes to only read metadata at first\n", + "asset = item['mosaic']\n", + "da = asset.to_dask()\n", + "da" ] }, { @@ -198,7 +157,10 @@ "metadata": {}, "outputs": [], "source": [ - "stacked = landsat['LC80110312014230LGN00'].stack_bands(['B1', 'B2'])" + "# The chunk structure isn't set by default\n", + "# setting a chunk structure makes operating on subsets of pixels more efficient\n", + "da = da.chunk(dict(band=1, x=2560, y=2560))\n", + "da" ] }, { @@ -207,8 +169,7 @@ "metadata": {}, "outputs": [], "source": [ - "stacked_da = stacked.to_dask()\n", - "stacked_da" + "da.isel(band=0, x=slice(0, 2560), y=slice(0, 1280)).plot.imshow()" ] }, { @@ -221,9 +182,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python [conda env:intake-stac-dev]", + "display_name": "Python 3", "language": "python", - "name": "conda-env-intake-stac-dev-py" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -235,9 +196,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.3" + "version": "3.7.8" } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/intake_stac/catalog.py b/intake_stac/catalog.py index 2ea92f2..e892b47 100644 --- a/intake_stac/catalog.py +++ b/intake_stac/catalog.py @@ -25,7 +25,7 @@ def __init__(self, stac_obj, **kwargs): stac_obj: stastac.Thing A satstac.Thing pointing to a STAC object kwargs : dict, optional - Passed to intake.Catolog.__init__ + Passed to intake.Catalog.__init__ """ if isinstance(stac_obj, self._stac_cls): self._stac_obj = stac_obj @@ -78,6 +78,7 @@ def serialize(self): class StacCatalog(AbstractStacCatalog): """ Intake Catalog represeting a STAC Catalog + https://github.com/radiantearth/stac-spec/blob/master/catalog-spec/catalog-spec.md A Catalog that references a STAC catalog at some URL and constructs an intake catalog from it, with opinionated @@ -97,21 +98,35 @@ def _load(self): """ Load the STAC Catalog. """ - for collection in self._stac_obj.collections(): - self._entries[collection.id] = LocalCatalogEntry( - name=collection.id, - description=collection.title, - driver=StacCollection, + subcatalog = None + # load first sublevel catalog(s) + for subcatalog in self._stac_obj.children(): + self._entries[subcatalog.id] = LocalCatalogEntry( + name=subcatalog.id, + description=subcatalog.description, + driver=StacCatalog, catalog=self, - args={'stac_obj': collection.filename}, + args={'stac_obj': subcatalog.filename}, ) + if subcatalog is None: + # load items under last catalog + for item in self._stac_obj.items(): + self._entries[item.id] = LocalCatalogEntry( + name=item.id, + description='', + driver=StacItem, + catalog=self, + args={'stac_obj': item}, + ) + def _get_metadata(self, **kwargs): - return dict( - description=self._stac_obj.description, - stac_version=self._stac_obj.stac_version, - **kwargs, - ) + """ + Keep copy of all STAC JSON except for links + """ + metadata = self._stac_obj._data.copy() + del metadata['links'] + return metadata class StacItemCollection(AbstractStacCatalog): @@ -169,6 +184,7 @@ def to_geopandas(self, crs=None): class StacCollection(AbstractStacCatalog): """ Intake Catalog represeting a STAC Collection + https://github.com/radiantearth/stac-spec/blob/master/collection-spec/collection-spec.md """ name = 'stac_collection' @@ -188,10 +204,7 @@ def _load(self): ) def _get_metadata(self, **kwargs): - metadata = getattr(self._stac_obj, 'properties', {}) - if metadata: - metadata = metadata.copy() - + metadata = {} for attr in [ 'title', 'version', @@ -208,6 +221,7 @@ def _get_metadata(self, **kwargs): class StacItem(AbstractStacCatalog): """ Intake Catalog represeting a STAC Item + https://github.com/radiantearth/stac-spec/blob/master/item-spec/item-spec.md """ name = 'stac_item' @@ -227,39 +241,68 @@ def _get_metadata(self, **kwargs): metadata.update(kwargs) return metadata + def _get_band_info(self): + """ + helper function for stack_bands + """ + # Try to get band-info at Collection then Item level + band_info = [] + try: + collection = self._stac_obj.collection() + if 'item-assets' in collection._data.get('stac_extensions'): + for val in collection._data['item_assets'].values(): + if 'eo:bands' in val: + band_info.append(val.get('eo:bands')[0]) + else: + band_info = collection.summaries['eo:bands'] + + except KeyError: + for val in self._stac_obj.assets.values(): + if 'eo:bands' in val: + band_info.append(val.get('eo:bands')[0]) + finally: + if not band_info: + raise AttributeError( + 'Unable to parse "eo:bands" information from STAC Collection or Item Assets' + ) + return band_info + def stack_bands(self, bands, regrid=False): """ Stack the listed bands over the ``band`` dimension. + This method only works for STAC Items using the 'eo' Extension + https://github.com/radiantearth/stac-spec/tree/master/extensions/eo + Parameters ---------- bands : list of strings representing the different bands - (e.g. ['B1', B2']). + (e.g. ['B4', B5'], ['red', 'nir']). Returns ------- - Catalog entry containing listed bands with ``band`` as a dimension - and coordinate. + StacEntry with mapping of Asset names to Xarray bands + Example + ------- + stack = item.stack_bands(['nir','red']) + da = stack(chunks=dict(band=1, x=2048, y=2048)).to_dask() """ - item = {'concat_dim': 'band', 'urlpath': [], 'type': 'image/x.geotiff'} - titles = [] - assets = self._stac_obj.assets - try: - band_info = self._stac_obj.collection().properties.get('eo:bands') - except AttributeError: - # TODO: figure out why satstac objects don't always have a - # collection. This workaround covers the case where - # `.collection()` returns None - band_info = self._stac_obj.properties.get('eo:bands') + if 'eo' not in self._stac_obj._data['stac_extensions']: + raise AttributeError('STAC Item must implement "eo" extension to use this method') + band_info = self._get_band_info() + item = {'concat_dim': 'band', 'urlpath': []} + titles = [] + types = [] + assets = self._stac_obj.assets for band in bands: # band can be band id, name or common_name if band in assets: info = next((b for b in band_info if b.get('id', b.get('name')) == band), None,) else: - info = next((b for b in band_info if b['common_name'] == band), None) + info = next((b for b in band_info if b.get('common_name') == band), None) if info is not None: band = info.get('id', info.get('name')) @@ -275,11 +318,7 @@ def stack_bands(self, bands, regrid=False): value = assets.get(band) band_type = value.get('type') - if band_type != item['type']: - raise ValueError( - f'Stacking failed: {band} has type {band_type} and ' - f'bands must have type {item["type"]}' - ) + types.append(band_type) href = value.get('href') pattern = href.replace(band, '{band}') @@ -302,9 +341,17 @@ def stack_bands(self, bands, regrid=False): f'({item["gsd"]})' ) - titles.append(value.get('title')) + titles.append(band) item['urlpath'].append(href) + unique_types = set(types) + if len(unique_types) != 1: + raise ValueError( + f'Stacking failed: bands must have type, multiple found: {unique_types}' + ) + else: + item['type'] = types[0] + item['title'] = ', '.join(titles) return StacEntry('_'.join(bands), item, stacked=True) diff --git a/requirements-dev.txt b/requirements-dev.txt index c925738..09ea1b6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,3 @@ pytest>=4.5.0 pytest-cov>=2.7.1 -geopandas -r requirements.txt diff --git a/requirements.txt b/requirements.txt index 9ebcd8e..e7571ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +aiohttp intake>=0.5.1 intake-xarray>=0.3 -sat-stac>=0.1.3 +sat-stac==0.4.*