├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── LICENSE ├── README.md ├── docs ├── Makefile ├── api.md ├── conf.py ├── index.md └── make.bat ├── pyproject.toml ├── setup.cfg ├── stac_vrt.py └── tests ├── Untitled.ipynb ├── expected.vrt ├── response-fixed.json ├── response.json └── test_stac_vrt.py /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [3.7, 3.8] 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip flit 21 | pip install .[test] 22 | 23 | - name: Test 24 | run: | 25 | python -m pytest 26 | 27 | lint: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v2 31 | - name: Set up Python 3.8 32 | uses: actions/setup-python@v2 33 | with: 34 | python-version: 3.8 35 | - name: Run pre-commit 36 | uses: pre-commit/action@v2.0.0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .coverage 3 | htmlcov 4 | dist 5 | docs/_build 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 20.8b1 4 | hooks: 5 | - id: black 6 | language_version: python3 7 | - repo: https://gitlab.com/pycqa/flake8 8 | rev: 3.8.4 9 | hooks: 10 | - id: flake8 11 | language_version: python3 12 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | python: 4 | version: 3.8 5 | install: 6 | - method: pip 7 | path: . 8 | extra_requirements: 9 | - docs 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Tom Augspurger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stac-vrt 2 | 3 | Build a GDAL VRT from a STAC response. 4 | 5 | [![Documentation Status](https://readthedocs.org/projects/stac-vrt/badge/?version=latest)](https://stac-vrt.readthedocs.io/en/latest/?badge=latest) 6 | 7 | ## Other Libraries 8 | 9 | stac-vrt is lightly maintained these days, and its use case is now better filled by other libraries: 10 | 11 | 1. GDAL now natively supports STAC items: See 12 | 2. [stackstac](https://stackstac.readthedocs.io/en/latest/) provides a nicer way to stack STAC items into a DataArray 13 | 14 | 15 | ## Example 16 | 17 | ```python 18 | >>> import stac_vrt, requests, rasterio 19 | >>> stac_items = requests.get( 20 | ... "http://pct-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip/items" 21 | ... ).json()["features"] 22 | >>> vrt = stac_vrt.build_vrt(stac_items, data_type="Byte", block_width=512, block_height=512) 23 | >>> ds = rasterio.open(vrt) 24 | >>> ds.shape 25 | (196870, 83790) 26 | ``` 27 | 28 | See [the documentation](https://stac-vrt.readthedocs.io/en/latest/) for more. 29 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= "-W" 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ```{eval-rst} 4 | .. autofunction:: stac_vrt.build_vrt 5 | ``` -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = "stac-vrt" 21 | copyright = "2021, Tom Augspurger" 22 | author = "Tom Augspurger" 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = "1.0.1" 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | "myst_parser", 35 | "sphinx.ext.autodoc", 36 | "sphinx.ext.intersphinx", 37 | "numpydoc", 38 | ] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ["_templates"] 42 | 43 | # List of patterns, relative to source directory, that match files and 44 | # directories to ignore when looking for source files. 45 | # This pattern also affects html_static_path and html_extra_path. 46 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 47 | 48 | # -- Options for HTML output ------------------------------------------------- 49 | 50 | # The theme to use for HTML and HTML Help pages. See the documentation for 51 | # a list of builtin themes. 52 | # 53 | html_theme = "pydata_sphinx_theme" 54 | 55 | # Add any paths that contain custom static files (such as style sheets) here, 56 | # relative to this directory. They are copied after the builtin static files, 57 | # so a file named "default.css" will overwrite the builtin "default.css". 58 | html_static_path = [] 59 | 60 | 61 | intersphinx_mapping = { 62 | "rasterio": ("https://rasterio.readthedocs.io/en/latest/", None), 63 | "rioxarray": ("https://corteva.github.io/rioxarray/stable/", None), 64 | } 65 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # stac-vrt 2 | 3 | ```{note} 4 | stac-vrt is lightly maintained these days, and its use case is now better filled by other libraries: 5 | 6 | 1. GDAL now natively supports STAC items: See 7 | 2. [stackstac](https://stackstac.readthedocs.io/en/latest/) provides a nicer way to stack STAC items into a DataArray 8 | ``` 9 | 10 | 11 | `stac-vrt` is a small library for quickly generating a [GDAL VRT][vrt] from a collection 12 | of [STAC][stac] items. This makes it fast and easy to generate a mosaic of many 13 | raster images. 14 | 15 | ## Installation 16 | 17 | `stac-vrt` can be installed from conda-forge 18 | 19 | conda install -c conda-forge stac-vrt 20 | 21 | or from PyPI 22 | 23 | pip install stac-vrt 24 | 25 | ## Usage 26 | 27 | {func}`stac_vrt.build_vrt` is the primary function to use. You provide it a list of [STAC items](https://github.com/radiantearth/stac-spec/tree/master/item-spec): 28 | 29 | ```python 30 | >>> import stac_vrt 31 | >>> import requests 32 | >>> stac_items = requests.get( 33 | ... "http://pct-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip/items" 34 | ... ).json()["features"] 35 | ``` 36 | 37 | These STAC items contain essentially all of the information needed to build a VRT. 38 | 39 | ```python 40 | >>> vrt = stac_vrt.build_vrt(stac_items, data_type="Byte", block_width=512, block_height=512) 41 | ``` 42 | 43 | The `vrt` variable is just a Python string that's a valid VRT (an XML document). It can 44 | be written to disk, or passed directly to [rasterio](https://rasterio.readthedocs.io/en/latest/) or [rioxarray](https://corteva.github.io/rioxarray/stable/) (this example uses `rioxarray` and also requires [Dask](https://dask.org/) to read the chunks in parallel). 45 | 46 | ```python 47 | >>> import rioxarray 48 | >>> ds = rioxarray.open_rasterio(vrt, chunks=(4, -1, "auto")) 49 | >>> ds 50 | 51 | dask.array, shape=(4, 11588, 20704), dtype=uint8, chunksize=(1, 11520, 11520), chunktype=numpy.ndarray> 52 | Coordinates: 53 | * band (band) int64 1 2 3 4 54 | * y (y) float64 2.986e+06 2.986e+06 2.986e+06 ... 2.98e+06 2.98e+06 55 | * x (x) float64 5.248e+05 5.248e+05 ... 5.372e+05 5.372e+05 56 | spatial_ref int64 0 57 | Attributes: 58 | scale_factor: 1.0 59 | add_offset: 0.0 60 | grid_mapping: spatial_ref 61 | ``` 62 | 63 | ## Background 64 | 65 | VRTs are a pretty cool concept in GDAL. The basic idea is to make document that's essentially just metadata; it points to *other* documents or URLs for the actual data. They're extremely useful for creating a mosiac of many images: the VRT just has information like "this sub-dataset goes at position `(x, y)` in the full dataset". 66 | 67 | VRTs pair extremely nicely with Dask-backed xarray DataArrays: you build up a mosaic of a whole bunch of images that just involves reading some metadata and doing some geospatial reprojections. No actual data is read. Then you can (lazily) read the actual data into an xarray DataArray for your analysis, and the separate original images can be read into separate chunks. 68 | 69 | One downside to (large) VRTs is that they can be time-consuming to build. You'd need to make at least one HTTP requests for each file going into the VRT to read the metadata (things like the CRS, shape, and transformation). 70 | 71 | When you're using STAC to discover your assets, you *already* have all of that information avaiable. And so `stac-vrt` is able to build the VRT without any additional network requests. An informal benchmark on a set of 500 images stored in Azure Blob Storage showed that `gdal.BuildVRT` took about 90 seconds, while `stac-vrt.build_vrt` took a handful of milliseconds. 72 | 73 | ## Building stac-vrt compatible STAC Items 74 | 75 | If you're responsible for creating STAC items, `stac-vrt` would appreciate if you include 76 | 77 | * [`proj:epsg`](https://github.com/radiantearth/stac-spec/blob/dev/extensions/projection/README.md#projepsg) 78 | * [`proj:shape`](https://github.com/radiantearth/stac-spec/blob/dev/extensions/projection/README.md#projshape) 79 | * [`proj:bbox`](https://github.com/radiantearth/stac-spec/blob/dev/extensions/projection/README.md#projbbox) 80 | * [`proj:transform`](https://github.com/radiantearth/stac-spec/blob/dev/extensions/projection/README.md#projtransform) 81 | 82 | These are used by `stac-vrt` to build the VRT. 83 | 84 | [vrt]: https://gdal.org/drivers/raster/vrt.html 85 | [stac]: https://stacspec.org/ 86 | 87 | ```{toctree} 88 | api 89 | ``` 90 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [tool.flit.metadata] 6 | module = "stac_vrt" 7 | author = "Tom Augspurger" 8 | author-email = "taugspurger@microsoft.com" 9 | classifiers = ["License :: OSI Approved :: MIT License"] 10 | requires-python=">=3.7" 11 | requires = [ 12 | "affine", 13 | "rasterio", 14 | "numpy", 15 | ] 16 | description-file="README.md" 17 | 18 | [tool.flit.metadata.requires-extra] 19 | test = [ 20 | "pytest>=6", 21 | ] 22 | docs = [ 23 | "myst-parser>=0.13.5", 24 | "pydata-sphinx-theme", 25 | "numpydoc", 26 | ] 27 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | extend-ignore = E203, W503 -------------------------------------------------------------------------------- /stac_vrt.py: -------------------------------------------------------------------------------- 1 | """ 2 | Quickly build a GDAL VRT from a STAC Item Collection. 3 | """ 4 | __version__ = "1.0.3" 5 | 6 | from typing import List, Optional, Tuple 7 | 8 | import affine 9 | import rasterio 10 | import rasterio.warp 11 | import numpy as np 12 | 13 | # TODO: change to types to a Protocol 14 | 15 | 16 | def _build_bboxes( 17 | stac_items, crs: rasterio.crs.CRS 18 | ) -> List[rasterio.coords.BoundingBox]: 19 | has_proj_bbox = "proj:bbox" in stac_items[0]["properties"] 20 | if has_proj_bbox: 21 | bboxes = [ 22 | rasterio.coords.BoundingBox(*item["properties"]["proj:bbox"]) 23 | for item in stac_items 24 | ] 25 | else: 26 | bboxes = [] 27 | for stac_item in stac_items: 28 | bboxes.append( 29 | rasterio.coords.BoundingBox( 30 | *rasterio.warp.transform_bounds( 31 | "epsg:4326", crs, *stac_item["bbox"] 32 | ) 33 | ) 34 | ) 35 | return bboxes 36 | 37 | 38 | def _build_bbox(bboxes): 39 | arr = np.array(bboxes) 40 | minima = arr.min(0) 41 | maxima = arr.max(0) 42 | 43 | out_bbox = rasterio.coords.BoundingBox(minima[0], minima[1], maxima[2], maxima[3]) 44 | return out_bbox 45 | 46 | 47 | def build_transform( 48 | bbox: rasterio.coords.BoundingBox, res_x: float, res_y: float 49 | ) -> affine.Affine: 50 | """ 51 | Build the geo transform from the bounding box of the output dataset. 52 | 53 | Notes 54 | ----- 55 | This uses `affine.Affine`. Convert to GDAL prior to writing the VRT. 56 | It currently assumes that the "rotation" values are 0. What are those? 57 | """ 58 | out_transform = affine.Affine(res_x, 0, bbox.left, 0, -res_y, bbox.top) 59 | return out_transform 60 | 61 | 62 | # TODO: figure out dataAxisToSRSAxisMapping 63 | _vrt_template = """\ 64 | 65 | {srs} 66 | {transform} 67 | {raster_bands} 68 | 69 | """ 70 | 71 | _raster_band_template = """\ 72 | {color_interp} 73 | {simple_sources} 74 | 75 | """ 76 | 77 | # TODO: Check assumptions on block width matching xSize in srcRect. 78 | # i.e. always reading all of the input. 79 | 80 | _simple_source_template = """\ 81 | 82 | {url} 83 | {band_number} 84 | 86 | 87 | 88 | 89 | """ 90 | 91 | 92 | def build_vrt( 93 | stac_items, 94 | *, 95 | crs: Optional[rasterio.crs.CRS] = None, 96 | res_x: Optional[float] = None, 97 | res_y: Optional[float] = None, 98 | shapes: Optional[List[Tuple[int, int]]] = None, 99 | bboxes: Optional[List[rasterio.coords.BoundingBox]] = None, 100 | data_type=None, 101 | block_width=None, 102 | block_height=None, 103 | add_prefix=True, 104 | ): 105 | """ 106 | Build a `GDAL VRT `_ from STAC Metadata. 107 | 108 | This can be used to quickly build a mosaic of COGs without needed to open each 109 | source file to query its metadata. 110 | 111 | Parameters 112 | ---------- 113 | stac_items : list 114 | This should be a list of dicts, compatible with the 115 | `stac-pydantic `_ ItemCollection 116 | model. The items should have the following `proj` STAC extension items 117 | 118 | 1. `proj:epsg` 119 | 2. `proj:transform` 120 | 3. `proj:shape` 121 | 4. `proj:bbox` 122 | 5. `eo:bands` 123 | 124 | If `proj:epsg` is present, all items must have the same epsg. 125 | 126 | crs : rasterio.crs.CRS, optional 127 | The CRS for the output VRT. Taken from the first STAC item's `proj:epsg` 128 | if not provided. This is used for the generated VRT's SRS field, after 129 | being converted to GDAL's WKT format. 130 | 131 | res_x, res_y: float, optional 132 | The resolution in the x and y directions. If not specified, this will 133 | be taken from the first STAC item's `proj:transform` field. 134 | shapes : list of tuples, optional 135 | The shape of each STAC item's asset, as ``(height, width)``. If not 136 | provided then this is taken from `proj:shape`. 137 | bboxes : list of tuples, optional 138 | The bounding box of each STAC item in its projected space. Note this is 139 | *not* the same as the STAC item's ``bbox`` field. Rather, it is the 140 | ``proj:bbox`` field. 141 | 142 | If provided, this should be a list of :class:`rasterio.coords.BoundingBox` 143 | namedtuples the same length as `stac_items`. If not provided this will 144 | be taken from 145 | 146 | 1. ``proj:bbox`` if present. Otherwise, 147 | 2. ``bbox``, reprojected to ``crs``. 148 | 149 | data_type : str 150 | The GDAL raster band data type of the data. 151 | https://gdal.org/user/raster_data_model.html#raster-band 152 | block_width, block_height : int 153 | The internal tiling of each COG. 154 | add_prefix : bool 155 | Whether to add the GDAL prefix to each href's source. By default, 156 | ``/vsicurl`` is prepended to http(s) prefixes. 157 | 158 | Returns 159 | ------- 160 | vrt : str 161 | The VRT as a string. This should be a valid XML file. It can also 162 | be passed directly to :meth:`rasterio.open` or 163 | :meth:`rioxarray.open_rasterio`. 164 | """ 165 | if not stac_items: 166 | raise ValueError("Must provide at least one stac item to 'build_vrt'.") 167 | 168 | if crs is None: 169 | try: 170 | crs = rasterio.crs.CRS.from_epsg(stac_items[0]["properties"]["proj:epsg"]) 171 | except KeyError as e: 172 | raise KeyError( 173 | "If 'crs' is not provided then it should be present " 174 | "in the first STAC item's 'proj:epsg' field." 175 | ) from e 176 | 177 | crs_code = crs.to_epsg() 178 | 179 | if res_x is None or res_y is None: 180 | # TODO: proj:transform might not exist. 181 | trn = stac_items[0]["properties"]["proj:transform"] 182 | trn = affine.Affine(*trn[:6]) # may be 6 or 9 elements. 183 | res_x = res_x or trn[0] 184 | res_y = res_y or abs(trn[4]) 185 | 186 | if bboxes is None: 187 | bboxes = _build_bboxes(stac_items, crs) 188 | elif len(bboxes) != len(stac_items): 189 | raise ValueError( 190 | "Number of user-provided 'bboxes' does not match the number of " 191 | "'stac_items' ({} != {})".format(len(bboxes), len(stac_items)) 192 | ) 193 | 194 | if shapes is None: 195 | shapes = [stac_item["properties"]["proj:shape"] for stac_item in stac_items] 196 | 197 | elif len(shapes) != len(stac_items): 198 | raise ValueError( 199 | "Number of user-provided 'shapes' does not match the number of " 200 | "'stac_items' ({} != {})".format(len(shapes), len(stac_items)) 201 | ) 202 | 203 | out_bbox = _build_bbox(bboxes) 204 | out_transform = build_transform(out_bbox, res_x, res_y) 205 | inv_transform = ~out_transform 206 | out_width, out_height = map(int, inv_transform * (out_bbox.right, out_bbox.bottom)) 207 | 208 | simple_sources = [] 209 | 210 | image = stac_items[0]["assets"]["image"] 211 | simple_sources = [[] for _ in image["eo:bands"]] 212 | 213 | assert len(stac_items) == len(bboxes) == len(shapes) 214 | assert len(stac_items) == len(bboxes) == len(shapes) 215 | 216 | for i, (stac_item, bbox, (height, width)) in enumerate( 217 | zip(stac_items, bboxes, shapes) 218 | ): 219 | image_crs = stac_item.get("properties", {}).get("proj:epsg") 220 | if image_crs and image_crs != crs_code: 221 | raise ValueError( 222 | "STAC item {} (position {}) does not have the " 223 | "same CRS. {} != {}".format(stac_item["id"], i, image_crs, crs_code) 224 | ) 225 | image = stac_item["assets"]["image"] 226 | x_off, y_off = ~out_transform * (bbox.left, bbox.top) 227 | for j, band in enumerate(image["eo:bands"], 1): 228 | url = image["href"] 229 | if add_prefix and url.startswith("http"): 230 | url = "/vsicurl/" + url 231 | 232 | simple_sources[j - 1].append( 233 | _simple_source_template.format( 234 | url=url, 235 | band_number=j, 236 | width=width, 237 | height=height, 238 | data_type=data_type, 239 | block_width=block_width, 240 | block_height=block_height, 241 | x_off=int(x_off), 242 | y_off=int(y_off), 243 | ) 244 | ) 245 | 246 | sources = ["".join(x) for x in simple_sources] 247 | rendered_bands = [] 248 | for band_number, band in enumerate(image["eo:bands"], 1): 249 | color_interp = "\n {}".format(band["name"]) 250 | rendered_bands.append( 251 | _raster_band_template.format( 252 | simple_sources=sources[band_number - 1], 253 | data_type=data_type, 254 | band_number=band_number, 255 | color_interp=color_interp, 256 | ) 257 | ) 258 | 259 | transform = ", ".join(map(str, out_transform.to_gdal())) 260 | result = _vrt_template.format( 261 | width=out_width, 262 | height=out_height, 263 | srs=crs.to_wkt(), 264 | transform=transform, 265 | raster_bands="".join(rendered_bands), 266 | ) 267 | return result 268 | -------------------------------------------------------------------------------- /tests/Untitled.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 23, 6 | "id": "quality-lexington", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import json" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 24, 16 | "id": "final-generation", 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "with open('response.json') as f:\n", 21 | " response = json.load(f)" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 27, 27 | "id": "cathedral-software", 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "proj_epsg = 26917\n", 32 | "\n", 33 | "\n", 34 | "# TODO: remove when added to NAIP data\n", 35 | "res_x = res_y = 0.6\n", 36 | "\n", 37 | "# TODO: remove when added to NAIP data\n", 38 | "bboxes = [\n", 39 | " [530802.0, 2979348.0, 537426.0, 2986692.0],\n", 40 | " [524604.0, 2979336.0, 531222.0, 2986674.0],\n", 41 | "]\n", 42 | "\n", 43 | "# TODO: remove when added to NAIP data\n", 44 | "shapes = [\n", 45 | " [12240, 11040],\n", 46 | " [12230, 11030]\n", 47 | "]\n", 48 | "\n", 49 | "# TODO: Remove when added to STAC\n", 50 | "data_type = \"Byte\"\n", 51 | "\n", 52 | "# TODO: Remove when added to STAC\n", 53 | "block_width = 512\n", 54 | "block_height = 512\n", 55 | "\n", 56 | "transforms = [\n", 57 | " (0.6, 0.0, 530802.0, 0.0, -0.6, 2986692.0, 0.0, 0.0, 1.0),\n", 58 | " (0.6, 0.0, 524604.0, 0.0, -0.6, 2986674.0, 0.0, 0.0, 1.0),\n", 59 | "]" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": 30, 65 | "id": "familiar-carry", 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "for i, feature in enumerate(response[\"features\"]):\n", 70 | " feature[\"properties\"][\"proj:epsg\"] = proj_epsg\n", 71 | " feature[\"properties\"][\"proj:shape\"] = shapes[i]\n", 72 | " feature[\"properties\"][\"proj:transform\"] = transforms[i]" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": 31, 78 | "id": "colored-adobe", 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [ 82 | "with open(\"response-fixed.json\", \"w\") as f:\n", 83 | " json.dump(response, f)" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": 34, 89 | "id": "chubby-order", 90 | "metadata": {}, 91 | "outputs": [], 92 | "source": [ 93 | "with open(\"response-fixed.json\") as f:\n", 94 | " r = json.load(f)" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": 51, 100 | "id": "prompt-circuit", 101 | "metadata": {}, 102 | "outputs": [], 103 | "source": [ 104 | "trn = r['features'][0]['properties']['proj:transform']" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": 56, 110 | "id": "variable-information", 111 | "metadata": {}, 112 | "outputs": [ 113 | { 114 | "data": { 115 | "text/plain": [ 116 | "Affine(0.6, 0.0, 530802.0,\n", 117 | " 0.0, -0.6, 2986692.0)" 118 | ] 119 | }, 120 | "execution_count": 56, 121 | "metadata": {}, 122 | "output_type": "execute_result" 123 | } 124 | ], 125 | "source": [ 126 | "affine.Affine(*trn[:6])" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 50, 132 | "id": "stylish-compilation", 133 | "metadata": {}, 134 | "outputs": [ 135 | { 136 | "ename": "NameError", 137 | "evalue": "name 'trn' is not defined", 138 | "output_type": "error", 139 | "traceback": [ 140 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 141 | "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", 142 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtrn\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 143 | "\u001b[0;31mNameError\u001b[0m: name 'trn' is not defined" 144 | ] 145 | } 146 | ], 147 | "source": [ 148 | "len(trn)" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": 49, 154 | "id": "cooked-devil", 155 | "metadata": {}, 156 | "outputs": [], 157 | "source": [ 158 | "affine.Affine?" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": null, 164 | "id": "commercial-center", 165 | "metadata": {}, 166 | "outputs": [], 167 | "source": [ 168 | "k\n", 169 | "['']" 170 | ] 171 | } 172 | ], 173 | "metadata": { 174 | "kernelspec": { 175 | "display_name": "Python 3", 176 | "language": "python", 177 | "name": "python3" 178 | }, 179 | "language_info": { 180 | "codemirror_mode": { 181 | "name": "ipython", 182 | "version": 3 183 | }, 184 | "file_extension": ".py", 185 | "mimetype": "text/x-python", 186 | "name": "python", 187 | "nbconvert_exporter": "python", 188 | "pygments_lexer": "ipython3", 189 | "version": "3.8.5" 190 | } 191 | }, 192 | "nbformat": 4, 193 | "nbformat_minor": 5 194 | } 195 | -------------------------------------------------------------------------------- /tests/expected.vrt: -------------------------------------------------------------------------------- 1 | 2 | PROJCS["NAD83 / UTM zone 17N",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-81],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","26917"]] 3 | 5.2460400000000000e+05, 5.9999999999999998e-01, 0.0000000000000000e+00, 2.9866920000000000e+06, 0.0000000000000000e+00, -5.9999999999999998e-01 4 | 5 | Red 6 | 7 | /vsicurl/https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_60cm_2019/26080/m_2608003_ne_17_060_20191215.tif 8 | 1 9 | 10 | 11 | 12 | 13 | 14 | /vsicurl/https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_60cm_2019/26080/m_2608003_nw_17_060_20191215.tif 15 | 1 16 | 17 | 18 | 19 | 20 | 21 | 22 | Green 23 | 24 | /vsicurl/https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_60cm_2019/26080/m_2608003_ne_17_060_20191215.tif 25 | 2 26 | 27 | 28 | 29 | 30 | 31 | /vsicurl/https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_60cm_2019/26080/m_2608003_nw_17_060_20191215.tif 32 | 2 33 | 34 | 35 | 36 | 37 | 38 | 39 | Blue 40 | 41 | /vsicurl/https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_60cm_2019/26080/m_2608003_ne_17_060_20191215.tif 42 | 3 43 | 44 | 45 | 46 | 47 | 48 | /vsicurl/https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_60cm_2019/26080/m_2608003_nw_17_060_20191215.tif 49 | 3 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | /vsicurl/https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_60cm_2019/26080/m_2608003_ne_17_060_20191215.tif 58 | 4 59 | 60 | 61 | 62 | 63 | 64 | /vsicurl/https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_60cm_2019/26080/m_2608003_nw_17_060_20191215.tif 65 | 4 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /tests/response-fixed.json: -------------------------------------------------------------------------------- 1 | {"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"coordinates": [[[-80.625, 26.9375], [-80.625, 27.0], [-80.6875, 27.0], [-80.6875, 26.9375], [-80.625, 26.9375]]], "type": "Polygon"}, "properties": {"created": "2021-02-12T01:14:20Z", "updated": "2021-02-12T01:14:20Z", "providers": [{"name": "USDA Farm Service Agency", "roles": ["producer", "licensor"], "url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/"}], "datetime": "2019-12-15T00:00:00Z", "naip:state": "fl", "proj:epsg": 26917, "proj:shape": [12240, 11040], "proj:transform": [0.6, 0.0, 530802.0, 0.0, -0.6, 2986692.0, 0.0, 0.0, 1.0], "proj:bbox": [530802.0, 2979348.0, 537426.0, 2986692.0]}, "id": "fl_m_2608003_ne_17_060_20191215_20200113", "bbox": [-80.6875, 26.9375, -80.625, 27.0], "stac_version": "1.0.0-beta.2", "assets": {"image": {"title": "RGBIR COG tile", "href": "https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_60cm_2019/26080/m_2608003_ne_17_060_20191215.tif", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], "eo:bands": [{"name": "Red"}, {"name": "Green"}, {"name": "Blue"}, {"name": "NIR", "description": "near-infrared"}]}, "metadata": {"title": "FGDC Metdata", "href": "https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_fgdc_2019/26080/m_2608003_ne_17_060_20191215.txt", "type": "text/plain", "roles": ["metadata"]}, "thumbnail": {"title": "Thumbnail", "href": "https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_60cm_2019/26080/m_2608003_ne_17_060_20191215.200.jpg", "type": "image/jpeg", "roles": ["thumbnail"]}}, "links": [{"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip/items/fl_m_2608003_ne_17_060_20191215_20200113", "rel": "self", "type": "application/geo+json"}, {"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip", "rel": "parent", "type": "application/json"}, {"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip", "rel": "collection", "type": "application/json"}, {"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/", "rel": "root", "type": "application/json"}, {"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip/items/fl_m_2608003_ne_17_060_20191215_20200113/tiles", "rel": "alternate", "type": "application/json", "title": "tiles"}], "stac_extensions": ["eo", "projection"], "collection": "usda-naip"}, {"type": "Feature", "geometry": {"coordinates": [[[-80.6875, 26.9375], [-80.6875, 27.0], [-80.75, 27.0], [-80.75, 26.9375], [-80.6875, 26.9375]]], "type": "Polygon"}, "properties": {"created": "2021-02-12T01:14:20Z", "updated": "2021-02-12T01:14:20Z", "providers": [{"name": "USDA Farm Service Agency", "roles": ["producer", "licensor"], "url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/"}], "datetime": "2019-12-15T00:00:00Z", "naip:state": "fl", "proj:epsg": 26917, "proj:shape": [12230, 11030], "proj:transform": [0.6, 0.0, 524604.0, 0.0, -0.6, 2986674.0, 0.0, 0.0, 1.0], "proj:bbox": [524604.0, 2979336.0, 531222.0, 2986674.0]}, "id": "fl_m_2608003_nw_17_060_20191215_20200113", "bbox": [-80.75, 26.9375, -80.6875, 27.0], "stac_version": "1.0.0-beta.2", "assets": {"image": {"title": "RGBIR COG tile", "href": "https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_60cm_2019/26080/m_2608003_nw_17_060_20191215.tif", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], "eo:bands": [{"name": "Red"}, {"name": "Green"}, {"name": "Blue"}, {"name": "NIR", "description": "near-infrared"}]}, "metadata": {"title": "FGDC Metdata", "href": "https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_fgdc_2019/26080/m_2608003_nw_17_060_20191215.txt", "type": "text/plain", "roles": ["metadata"]}, "thumbnail": {"title": "Thumbnail", "href": "https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_60cm_2019/26080/m_2608003_nw_17_060_20191215.200.jpg", "type": "image/jpeg", "roles": ["thumbnail"]}}, "links": [{"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip/items/fl_m_2608003_nw_17_060_20191215_20200113", "rel": "self", "type": "application/geo+json"}, {"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip", "rel": "parent", "type": "application/json"}, {"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip", "rel": "collection", "type": "application/json"}, {"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/", "rel": "root", "type": "application/json"}, {"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip/items/fl_m_2608003_nw_17_060_20191215_20200113/tiles", "rel": "alternate", "type": "application/json", "title": "tiles"}], "stac_extensions": ["eo", "projection"], "collection": "usda-naip"}], "links": [{"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip/items?token=VgZLXbTz&limit=10", "rel": "next", "type": "application/geo+json", "method": "GET"}], "context": {"returned": 2, "matched": 961492}} -------------------------------------------------------------------------------- /tests/response.json: -------------------------------------------------------------------------------- 1 | {"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"coordinates": [[[-80.625, 26.9375], [-80.625, 27.0], [-80.6875, 27.0], [-80.6875, 26.9375], [-80.625, 26.9375]]], "type": "Polygon"}, "properties": {"created": "2021-02-12T01:14:20Z", "updated": "2021-02-12T01:14:20Z", "providers": [{"name": "USDA Farm Service Agency", "roles": ["producer", "licensor"], "url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/"}], "datetime": "2019-12-15T00:00:00Z", "naip:state": "fl", "proj:epsg": 32617}, "id": "fl_m_2608003_ne_17_060_20191215_20200113", "bbox": [-80.6875, 26.9375, -80.625, 27.0], "stac_version": "1.0.0-beta.2", "assets": {"image": {"title": "RGBIR COG tile", "href": "https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_60cm_2019/26080/m_2608003_ne_17_060_20191215.tif", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], "eo:bands": [{"name": "Red"}, {"name": "Green"}, {"name": "Blue"}, {"name": "NIR", "description": "near-infrared"}]}, "metadata": {"title": "FGDC Metdata", "href": "https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_fgdc_2019/26080/m_2608003_ne_17_060_20191215.txt", "type": "text/plain", "roles": ["metadata"]}, "thumbnail": {"title": "Thumbnail", "href": "https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_60cm_2019/26080/m_2608003_ne_17_060_20191215.200.jpg", "type": "image/jpeg", "roles": ["thumbnail"]}}, "links": [{"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip/items/fl_m_2608003_ne_17_060_20191215_20200113", "rel": "self", "type": "application/geo+json"}, {"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip", "rel": "parent", "type": "application/json"}, {"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip", "rel": "collection", "type": "application/json"}, {"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/", "rel": "root", "type": "application/json"}, {"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip/items/fl_m_2608003_ne_17_060_20191215_20200113/tiles", "rel": "alternate", "type": "application/json", "title": "tiles"}], "stac_extensions": ["eo", "projection"], "collection": "usda-naip"}, {"type": "Feature", "geometry": {"coordinates": [[[-80.6875, 26.9375], [-80.6875, 27.0], [-80.75, 27.0], [-80.75, 26.9375], [-80.6875, 26.9375]]], "type": "Polygon"}, "properties": {"created": "2021-02-12T01:14:20Z", "updated": "2021-02-12T01:14:20Z", "providers": [{"name": "USDA Farm Service Agency", "roles": ["producer", "licensor"], "url": "https://www.fsa.usda.gov/programs-and-services/aerial-photography/imagery-programs/naip-imagery/"}], "datetime": "2019-12-15T00:00:00Z", "naip:state": "fl", "proj:epsg": 32617}, "id": "fl_m_2608003_nw_17_060_20191215_20200113", "bbox": [-80.75, 26.9375, -80.6875, 27.0], "stac_version": "1.0.0-beta.2", "assets": {"image": {"title": "RGBIR COG tile", "href": "https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_60cm_2019/26080/m_2608003_nw_17_060_20191215.tif", "type": "image/tiff; application=geotiff; profile=cloud-optimized", "roles": ["data"], "eo:bands": [{"name": "Red"}, {"name": "Green"}, {"name": "Blue"}, {"name": "NIR", "description": "near-infrared"}]}, "metadata": {"title": "FGDC Metdata", "href": "https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_fgdc_2019/26080/m_2608003_nw_17_060_20191215.txt", "type": "text/plain", "roles": ["metadata"]}, "thumbnail": {"title": "Thumbnail", "href": "https://naipeuwest.blob.core.windows.net/naip/v002/fl/2019/fl_60cm_2019/26080/m_2608003_nw_17_060_20191215.200.jpg", "type": "image/jpeg", "roles": ["thumbnail"]}}, "links": [{"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip/items/fl_m_2608003_nw_17_060_20191215_20200113", "rel": "self", "type": "application/geo+json"}, {"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip", "rel": "parent", "type": "application/json"}, {"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip", "rel": "collection", "type": "application/json"}, {"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/", "rel": "root", "type": "application/json"}, {"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip/items/fl_m_2608003_nw_17_060_20191215_20200113/tiles", "rel": "alternate", "type": "application/json", "title": "tiles"}], "stac_extensions": ["eo", "projection"], "collection": "usda-naip"}], "links": [{"href": "http://pc-mqe-staging.westeurope.cloudapp.azure.com/collections/usda-naip/items?token=VgZLXbTz&limit=10", "rel": "next", "type": "application/geo+json", "method": "GET"}], "context": {"returned": 2, "matched": 961492}} -------------------------------------------------------------------------------- /tests/test_stac_vrt.py: -------------------------------------------------------------------------------- 1 | import json 2 | import io 3 | from pathlib import Path 4 | import xml.etree 5 | 6 | import pytest 7 | import rasterio.coords 8 | 9 | import stac_vrt 10 | 11 | HERE = Path(__name__).parent.absolute() 12 | 13 | 14 | @pytest.fixture 15 | def response(): 16 | with open(HERE / "tests/response.json") as f: 17 | resp = json.load(f) 18 | 19 | return resp 20 | 21 | 22 | def assert_vrt_equal(result, expected): 23 | for path in [ 24 | ".", 25 | "SRS", 26 | "GeoTransform", 27 | "VRTRasterBand", 28 | "VRTRasterBand/ColorInterp", 29 | "VRTRasterBand/SimpleSource", 30 | "VRTRasterBand/SimpleSource/SourceFilename", 31 | "VRTRasterBand/SimpleSource/SourceBand", 32 | "VRTRasterBand/SimpleSource/SourceProperties", 33 | "VRTRasterBand/SimpleSource/SrcRect", 34 | "VRTRasterBand/SimpleSource/DstRect", 35 | ]: 36 | rchild = result.findall(path) 37 | echild = expected.findall(path) 38 | 39 | assert len(echild) 40 | if path != "VRTRasterBand/ColorInterp": 41 | # TODO: check on the expected. 42 | assert len(echild) == len(rchild) 43 | 44 | for a, b in zip(echild, rchild): 45 | assert a.attrib == b.attrib 46 | if path == "GeoTransform": 47 | x = list(map(lambda x: round(float(x)), a.text.split(","))) 48 | y = list(map(lambda x: round(float(x)), a.text.split(","))) 49 | assert x == y 50 | 51 | else: 52 | assert a.text == b.text 53 | 54 | 55 | def test_fixture(response): 56 | assert set(response) == {"context", "features", "links", "type"} 57 | 58 | 59 | def test_integration(response): 60 | stac_items = response["features"] 61 | # TODO: remove when fixed in NAIP data 62 | # Have to at least fix the CRS.... 63 | for item in stac_items: 64 | item["properties"]["proj:epsg"] = 26917 65 | crs = rasterio.crs.CRS.from_string("epsg:26917") 66 | 67 | # TODO: remove when added to NAIP data 68 | res_x = res_y = 0.6 69 | 70 | # TODO: remove when added to NAIP data 71 | bboxes = [ 72 | rasterio.coords.BoundingBox( 73 | left=530802.0, bottom=2979348.0, right=537426.0, top=2986692.0 74 | ), 75 | rasterio.coords.BoundingBox( 76 | left=524604.0, bottom=2979336.0, right=531222.0, top=2986674.0 77 | ), 78 | ] 79 | 80 | # TODO: remove when added to NAIP data 81 | shapes = [(12240, 11040), (12230, 11030)] 82 | 83 | # TODO: Remove when added to STAC 84 | data_type = "Byte" 85 | 86 | # TODO: Remove when added to STAC 87 | block_width = 512 88 | block_height = 512 89 | 90 | # -------------------------- 91 | # Now for the test. 92 | result = stac_vrt.build_vrt( 93 | stac_items, 94 | crs=crs, 95 | res_x=res_x, 96 | res_y=res_y, 97 | shapes=shapes, 98 | bboxes=bboxes, 99 | data_type=data_type, 100 | block_width=block_width, 101 | block_height=block_height, 102 | ) 103 | 104 | expected_tree = xml.etree.ElementTree.parse(HERE / "tests/expected.vrt").getroot() 105 | result_tree = xml.etree.ElementTree.parse(io.StringIO(result)).getroot() 106 | assert_vrt_equal(result_tree, expected_tree) 107 | 108 | 109 | def test_integration_fixed(): 110 | with open(HERE / "tests/response-fixed.json") as f: 111 | resp = json.load(f) 112 | 113 | stac_items = resp["features"] 114 | vrt = stac_vrt.build_vrt( 115 | stac_items, data_type="Byte", block_width=512, block_height=512 116 | ) 117 | 118 | ds = rasterio.open(vrt) 119 | ds.transform 120 | 121 | 122 | def test_no_items(): 123 | with pytest.raises(ValueError, match="Must provide"): 124 | stac_vrt.build_vrt([]) 125 | 126 | 127 | def test_incorrect_bboxes(): 128 | with pytest.raises(ValueError, match="2 != 1"): 129 | stac_vrt.build_vrt( 130 | [{"test": 1}], 131 | bboxes=[[1, 2, 3, 4], [5, 6, 7, 8]], 132 | crs=rasterio.crs.CRS.from_string("epsg:26917"), 133 | res_x=1, 134 | res_y=1, 135 | ) 136 | 137 | 138 | def test_incorrect_shapes(): 139 | with pytest.raises(ValueError, match="2 != 1"): 140 | stac_vrt.build_vrt( 141 | [{"test": 1}], 142 | bboxes=[[1, 2, 3, 4]], 143 | shapes=[[1, 2], [3, 4]], 144 | crs=rasterio.crs.CRS.from_string("epsg:26917"), 145 | res_x=1, 146 | res_y=1, 147 | ) 148 | 149 | 150 | def test_multiple_crs_raises(): 151 | with open(HERE / "tests/response-fixed.json") as f: 152 | resp = json.load(f) 153 | resp["features"][0]["properties"]["proj:epsg"] = 26918 154 | 155 | with pytest.raises(ValueError, match="same CRS"): 156 | stac_vrt.build_vrt( 157 | resp["features"], data_type="Byte", block_width=512, block_height=512 158 | ) 159 | 160 | 161 | def test_missing_crs_raises(): 162 | with open(HERE / "tests/response-fixed.json") as f: 163 | resp = json.load(f) 164 | del resp["features"][0]["properties"]["proj:epsg"] 165 | 166 | with pytest.raises(KeyError, match="proj:epsg"): 167 | stac_vrt.build_vrt( 168 | resp["features"], data_type="Byte", block_width=512, block_height=512 169 | ) 170 | --------------------------------------------------------------------------------