├── .github
├── codecov.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGES.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── examples
├── Display_tiles_in_notebook.ipynb
└── STAC_in_notebook.ipynb
├── pyproject.toml
├── rio_viz
├── __init__.py
├── app.py
├── io
│ ├── __init__.py
│ ├── mosaic.py
│ └── reader.py
├── resources
│ ├── __init__.py
│ └── enums.py
├── scripts
│ ├── __init__.py
│ └── cli.py
└── templates
│ ├── assets.html
│ ├── bands.html
│ ├── index.html
│ ├── map.html
│ └── wmts.xml
└── tests
├── fixtures
├── cog.tif
├── cogb1.tif
├── cogb2.tif
├── cogb3.tif
├── mosaic_cog1.tif
├── mosaic_cog2.tif
└── noncog.tif
├── test_app.py
├── test_cli.py
└── test_enums.py
/.github/codecov.yml:
--------------------------------------------------------------------------------
1 | comment: off
2 |
3 | coverage:
4 | status:
5 | project:
6 | default:
7 | target: auto
8 | threshold: 5
9 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | tags:
8 | - '*'
9 | pull_request:
10 | env:
11 | LATEST_PY_VERSION: '3.12'
12 |
13 | jobs:
14 | tests:
15 | runs-on: ubuntu-latest
16 | strategy:
17 | matrix:
18 | python-version:
19 | - '3.9'
20 | - '3.10'
21 | - '3.11'
22 | - '3.12'
23 | - '3.13'
24 |
25 | steps:
26 | - uses: actions/checkout@v4
27 | - name: Set up Python ${{ matrix.python-version }}
28 | uses: actions/setup-python@v5
29 | with:
30 | python-version: ${{ matrix.python-version }}
31 |
32 | - name: Install dependencies
33 | run: |
34 | python -m pip install --upgrade pip
35 | python -m pip install .["mvt,test"]
36 |
37 | - name: Run tests
38 | run: python -m pytest --cov rio_viz --cov-report xml --cov-report term-missing
39 |
40 | - name: run pre-commit
41 | if: ${{ matrix.python-version == env.LATEST_PY_VERSION }}
42 | run: |
43 | python -m pip install pre-commit
44 | pre-commit run --all-files
45 |
46 | - name: Upload Results
47 | if: ${{ matrix.python-version == env.LATEST_PY_VERSION }}
48 | uses: codecov/codecov-action@v1
49 | with:
50 | file: ./coverage.xml
51 | flags: unittests
52 | name: ${{ matrix.python-version }}
53 | fail_ci_if_error: false
54 |
55 | publish:
56 | needs: [tests]
57 | runs-on: ubuntu-latest
58 | if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release'
59 | steps:
60 | - uses: actions/checkout@v4
61 | - name: Set up Python
62 | uses: actions/setup-python@v5
63 | with:
64 | python-version: ${{ env.LATEST_PY_VERSION }}
65 |
66 | - name: Install dependencies
67 | run: |
68 | python -m pip install --upgrade pip
69 | python -m pip install hatch
70 | python -m hatch build
71 |
72 | - name: Set tag version
73 | id: tag
74 | run: |
75 | echo "version=${GITHUB_REF#refs/*/}"
76 | echo "version=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
77 |
78 | - name: Set module version
79 | id: module
80 | run: |
81 | hatch --quiet version
82 | echo "version=$(hatch --quiet version)" >> $GITHUB_OUTPUT
83 |
84 | - name: Build and publish
85 | if: ${{ steps.tag.outputs.version }} == ${{ steps.module.outputs.version}}
86 | env:
87 | HATCH_INDEX_USER: ${{ secrets.PYPI_USERNAME }}
88 | HATCH_INDEX_AUTH: ${{ secrets.PYPI_PASSWORD }}
89 | run: |
90 | python -m hatch publish
91 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 | db.sqlite3
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
106 | rio_viz/*.c
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/abravalheri/validate-pyproject
3 | rev: v0.12.1
4 | hooks:
5 | - id: validate-pyproject
6 |
7 | - repo: https://github.com/PyCQA/isort
8 | rev: 5.13.2
9 | hooks:
10 | - id: isort
11 | language_version: python
12 |
13 | - repo: https://github.com/astral-sh/ruff-pre-commit
14 | rev: v0.3.5
15 | hooks:
16 | - id: ruff
17 | args: ["--fix"]
18 | - id: ruff-format
19 |
20 | - repo: https://github.com/pre-commit/mirrors-mypy
21 | rev: v1.9.0
22 | hooks:
23 | - id: mypy
24 | language_version: python
25 | # No reason to run if only tests have changed. They intentionally break typing.
26 | exclude: tests/.*
27 | additional_dependencies:
28 | - types-attrs
29 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | # 0.14.0 (2025-03-20)
2 |
3 | * add `--geojson` option to add a GeoJSON Feature or FeatureCollection on the map viewer
4 |
5 | # 0.13.1 (2025-03-19)
6 |
7 | * relaxe titiler requirement to `>=0.20,<0.22`
8 |
9 | # 0.13.0 (2025-01-20)
10 |
11 | * remove python 3.8 support
12 | * update titiler dependency to `>=0.20,<0.21`
13 | * remove docker image publishing
14 |
15 | # 0.12.2 (2024-04-24)
16 |
17 | * update titiler dependency to `>=0.16,<0.19`
18 | * add python 3.12 official support
19 | * change code formatter to `ruff-format`
20 | * switch to Hatch
21 |
22 | # 0.12.1 (2024-01-12)
23 |
24 | * fix invalid nodata overwriting
25 |
26 | # 0.12.0 (2024-01-12)
27 |
28 | * add Algorithm support and update basemap source
29 | * update titiler requirement to `>=0.16,<0.17`
30 |
31 | - renamed `/crop` endpoints to `/bbox/...` or `/feature/...`
32 | - `/crop/{minx},{miny},{maxx},{maxy}.{format}` -> `/bbox/{minx},{miny},{maxx},{maxy}.{format}`
33 |
34 | - `/crop/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}` -> `/bbox/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}`
35 |
36 | - `/crop [POST]` -> `/feature [POST]`
37 |
38 | - `/crop.{format} [POST]` -> `/feature.{format} [POST]`
39 |
40 | - `/crop/{width}x{height}.{format} [POST]` -> `/feature/{width}x{height}.{format} [POST]`
41 |
42 |
43 | # 0.11.0 (2023-07-27)
44 |
45 | * update titiler requirement to `>=0.13,<0.14`
46 |
47 | # 0.10.3 (2023-03-22)
48 |
49 | * switch `/map` viewer to maplibre
50 |
51 | ## 0.10.2 (2023-03-20)
52 |
53 | * handle dateline crossing dataset in html viewers
54 | * fix issue with FastAPI>0.93
55 | * update FastAPI dependency version
56 |
57 | ## 0.10.1 (2023-03-01)
58 |
59 | * update titiler requirement
60 |
61 | ## 0.10.0 (2022-12-16)
62 |
63 | * remove `AsyncBaseReader` support
64 | * add Jupyter Notebook compatible `Client`
65 |
66 | ```python
67 | import time
68 |
69 | import httpx
70 | from folium import Map, TileLayer
71 |
72 | from rio_viz.app import Client
73 |
74 | # Create rio-viz Client (using server-thread to launch background task)
75 | client = Client("https://data.geo.admin.ch/ch.swisstopo.swissalti3d/swissalti3d_2019_2573-1085/swissalti3d_2019_2573-1085_0.5_2056_5728.tif")
76 |
77 | # Gives some time for the server to setup
78 | time.sleep(1)
79 |
80 | r = httpx.get(
81 | f"{client.endpoint}/tilejson.json",
82 | params = {
83 | "rescale": "1600,2000", # from the info endpoint
84 | "colormap_name": "terrain",
85 | }
86 | ).json()
87 |
88 | bounds = r["bounds"]
89 | m = Map(
90 | location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),
91 | zoom_start=r["minzoom"]
92 | )
93 |
94 | aod_layer = TileLayer(
95 | tiles=r["tiles"][0],
96 | opacity=1,
97 | attr="Yo!!"
98 | )
99 | aod_layer.add_to(m)
100 | m
101 | ```
102 |
103 | * update `rio-tiler-mvt` requirement (**>=0.1,<0.2**)
104 |
105 | * add support for python 3.10 and 3.11
106 | * remove python 3.7 support
107 | * update titiler requirement to `0.10`
108 | * revert using static files for javascript libraries
109 | * add simple `/map` viewer
110 |
111 | ## 0.9.6 (2022-06-14)
112 |
113 | * update `titiler` and `starlette-cramjam` requirements
114 |
115 | ## 0.9.5 (2022-05-17)
116 |
117 | * Fix viewers. Statistics wasn't being displayed for raster with band names (instead of indexes number).
118 |
119 | ## 0.9.4 (2022-05-16)
120 |
121 | * fix issue for MultiBaseReader (`assets`) viewer
122 | * update titiler.core requirement to `>=0.5,<0.7`
123 | * add `/` as path for the viewer
124 |
125 | ## 0.9.3 (2022-04-01)
126 |
127 | * fix frontend click/point interaction for dataset crossing the antimeridian line
128 |
129 | ## 0.9.2 (2022-03-29)
130 |
131 | * better handle data with bbox crossing the antimeridian limit
132 | * include js and css code within the package to enable full offline mode
133 | * switch to pyproject.toml
134 |
135 | ## 0.9.1 (2022-02-25)
136 |
137 | * add `color-formula` in viz
138 |
139 | ## 0.9.0 (2022-02-25)
140 |
141 | * update titiler requirement and fix viewer bugs
142 |
143 | ## 0.8.0 (2021-11-30)
144 |
145 | * update to new titiler/rio-tiler/rio-cogeo version
146 | * remove Mapbox maps and switch to `stamen` basemap
147 | * remove python3.6 support
148 |
149 | ## 0.7.2 (2021-09-23)
150 |
151 | * use titiler custom JSONResponse to encode nan/inf/-inf values in response
152 |
153 | ## 0.7.1 (2021-09-17)
154 |
155 | * do not rescale data if there is a colormap
156 |
157 | ## 0.7.0 (2021-07-16)
158 |
159 | * add `titiler.core` as dependencies to reduce code duplication.
160 | * update code and templates to follow `titiler.core` specs.
161 | * add `/crop.{format}` POST endpoint to get image from polygon shaped GeoJSON (https://github.com/developmentseed/rio-viz/pull/36)
162 | * rename `/part` to `/crop` to match TiTiler (https://github.com/developmentseed/rio-viz/pull/36)
163 | * refactor dependencies to remove bidx in info/metadata/stats (https://github.com/developmentseed/rio-viz/pull/37)
164 | * refactor UI (https://github.com/developmentseed/rio-viz/pull/38)
165 | * add simple **MosaicReader** (https://github.com/developmentseed/rio-viz/pull/32)
166 |
167 | ```bash
168 | $ rio viz "tests/fixtures/mosaic_cog{1,2}.tif" --reader rio_viz.io.MosaicReader
169 | ```
170 |
171 | ## 0.6.1 (2021-04-08)
172 |
173 | * update rio-tiler-mvt
174 | * use cache middleware to add `cache-control` headers.
175 |
176 | ## 0.6.0 (2021-03-23)
177 |
178 | * add dynamic dependency injection to better support multiple reader types (https://github.com/developmentseed/rio-viz/pull/28)
179 | * add better UI for MultiBaseReader (e.g STAC)
180 | * renamed `indexes` query parameter to `bidx`
181 | * update bands/assets/indexes query parameter style to follow the common specification
182 |
183 | ```
184 | # before
185 | /tiles/9/150/189?indexes=1,2,3
186 |
187 | # now
188 | /tiles/9/150/189?bidx=1&bidx=2&bidx=3
189 | ```
190 |
191 | ## 0.5.0 (2021-03-01)
192 |
193 | * renamed `rio_viz.ressources` to `rio_viz.resources` (https://github.com/developmentseed/titiler/pull/210)
194 | * update and reduce requirements
195 | * fix wrong tilesize setting in UI
196 | * update pre-commit configuration
197 |
198 | ## 0.4.4 (2021-01-27)
199 |
200 | * update requirements.
201 | * add Mapbox `dark` basemap as default.
202 |
203 | ## 0.4.3.post1 (2020-12-15)
204 |
205 | * add missing `__init__` in rio_viz.io submodule (https://github.com/developmentseed/rio-viz/pull/24)
206 |
207 | ## 0.4.3 (2020-12-15)
208 |
209 | * Fix error when `rio-tiler-mvt` is not installed (https://github.com/developmentseed/rio-viz/issues/21)
210 |
211 | ## 0.4.2 (2020-11-24)
212 |
213 | * update for rio-tiler 2.0.0rc3
214 |
215 | ## 0.4.1 (2020-11-17)
216 |
217 | * add `--server-only` options and add preview/part API.
218 | * add more output types.
219 |
220 | ## 0.4.0 (2020-11-09)
221 |
222 | **Refactor**
223 |
224 | * remove template factory
225 | * better FastAPI app definition (to be able to use it outside rio-viz)
226 | * remove `simple` template
227 | * use dataclasses
228 | * adapt for rio-tiler >= 2.0.0rc1
229 | * full async API
230 | * add `external` reader support
231 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Issues and pull requests are more than welcome.
4 |
5 | **dev install**
6 |
7 | ```bash
8 | $ git clone https://github.com/developmentseed/rio-viz.git
9 | $ cd rio-viz
10 | $ pip install -e .["test,dev"]
11 | ```
12 |
13 | You can then run the tests with the following command:
14 |
15 | ```sh
16 | python -m pytest --cov rio_viz --cov-report term-missing
17 | ```
18 |
19 | **pre-commit**
20 |
21 | This repo is set to use `pre-commit` to run *isort*, *flake8*, *pydocstring*, *black* ("uncompromising Python code formatter") and mypy when committing new code.
22 |
23 | ```bash
24 | $ pre-commit install
25 | ```
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Development Seed
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # rio-viz
2 |
3 |
4 |
5 |
A Rasterio plugin to visualize Cloud Optimized GeoTIFF in browser.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | ## Install
25 |
26 | You can install rio-viz using pip
27 |
28 | ```bash
29 | $ pip install rio-viz
30 | ```
31 | with 3d feature
32 |
33 | ```bash
34 | # 3d visualization features is optional
35 | $ pip install -U pip
36 | $ pip install rio-viz["mvt"]
37 | ```
38 |
39 | Build from source
40 |
41 | ```bash
42 | $ git clone https://github.com/developmentseed/rio-viz.git
43 | $ cd rio-viz
44 | $ pip install -e .
45 | ```
46 |
47 | ## CLI
48 |
49 | ```bash
50 | $ rio viz --help
51 | Usage: rio viz [OPTIONS] SRC_PATH
52 |
53 | Rasterio Viz cli.
54 |
55 | Options:
56 | --nodata NUMBER|nan Set nodata masking values for input dataset.
57 | --minzoom INTEGER Overwrite minzoom
58 | --maxzoom INTEGER Overwrite maxzoom
59 | --port INTEGER Webserver port (default: 8080)
60 | --host TEXT Webserver host url (default: 127.0.0.1)
61 | --no-check Ignore COG validation
62 | --reader TEXT rio-tiler Reader (BaseReader). Default is `rio_tiler.io.COGReader`
63 | --layers TEXT limit to specific layers (only used for MultiBand and MultiBase Readers). (e.g --layers b1 --layers b2).
64 | --server-only Launch API without opening the rio-viz web-page.
65 | --config NAME=VALUE GDAL configuration options.
66 | --help Show this message and exit.
67 | ```
68 |
69 | ## Multi Reader support
70 |
71 | rio-viz support multiple/custom reader as long they are subclass of `rio_tiler.io.base.BaseReader`.
72 |
73 | ```bash
74 | # Multi Files as Bands
75 | $ rio viz "cog_band{2,3,4}.tif" --reader rio_viz.io.MultiFilesBandsReader
76 |
77 | # Simple Mosaic
78 | $ rio viz "tests/fixtures/mosaic_cog{1,2}.tif" --reader rio_viz.io.MosaicReader
79 |
80 | # MultiBandReader
81 | # Landsat 8 - rio-tiler-pds
82 | # We use `--layers` to limit the number of bands
83 | $ rio viz LC08_L1TP_013031_20130930_20170308_01_T1 \
84 | --reader rio_tiler_pds.landsat.aws.landsat8.L8Reader \
85 | --layers B1,B2 \
86 | --config GDAL_DISABLE_READDIR_ON_OPEN=FALSE \
87 | --config CPL_VSIL_CURL_ALLOWED_EXTENSIONS=".TIF,.ovr"
88 |
89 | # MultiBaseReader
90 | # We use `--layers` to limit the number of assets
91 | rio viz https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs/items/S2A_34SGA_20200318_0_L2A \
92 | --reader rio_tiler.io.STACReader \
93 | --layers B04,B03,B02
94 | ```
95 |
96 | ## RestAPI
97 |
98 | When launching rio-viz, the application will create a FastAPI application to access and read the data you want. By default the CLI will open a web-page for you to explore your file but you can use `--server-only` option to ignore this.
99 |
100 | ```bash
101 | $ rio viz my.tif --server-only
102 |
103 | # In another console
104 | $ curl http://127.0.0.1:8080/info | jq
105 | {
106 | "bounds": [6.608576517072109, 51.270642883468895, 11.649386808679436, 53.89267160832534],
107 | "band_metadata": [...],
108 | "band_descriptions": [...],
109 | "dtype": "uint8",
110 | "nodata_type": "Mask",
111 | "colorinterp": [
112 | "red",
113 | "green",
114 | "blue"
115 | ]
116 | }
117 | ```
118 |
119 | You can see the full API documentation over `http://127.0.0.1:8080/docs`
120 |
121 | 
122 |
123 | ## In Notebook environment
124 |
125 | Thanks to the awesome [server-thread](https://github.com/banesullivan/server-thread) we can use `rio-viz` application in Notebook environment.
126 |
127 | ```python
128 | import time
129 |
130 | import httpx
131 | from folium import Map, TileLayer
132 |
133 | from rio_viz.app import Client
134 |
135 | # Create rio-viz Client (using server-thread to launch backgroud task)
136 | client = Client("https://data.geo.admin.ch/ch.swisstopo.swissalti3d/swissalti3d_2019_2573-1085/swissalti3d_2019_2573-1085_0.5_2056_5728.tif")
137 |
138 | # Gives some time for the server to setup
139 | time.sleep(1)
140 |
141 | r = httpx.get(
142 | f"{client.endpoint}/tilejson.json",
143 | params = {
144 | "rescale": "1600,2000", # from the info endpoint
145 | "colormap_name": "terrain",
146 | }
147 | ).json()
148 |
149 | bounds = r["bounds"]
150 | m = Map(
151 | location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),
152 | zoom_start=r["minzoom"]
153 | )
154 |
155 | aod_layer = TileLayer(
156 | tiles=r["tiles"][0],
157 | opacity=1,
158 | attr="Yo!!"
159 | )
160 | aod_layer.add_to(m)
161 | m
162 | ```
163 | 
164 |
165 |
166 | ## 3D (Experimental)
167 |
168 | rio-viz supports Mapbox VectorTiles encoding from a raster array. This feature was added to visualize sparse data stored as raster but will also work for dense array. This is highly experimental and might be slow to render in certain browser and/or for big rasters.
169 |
170 | 
171 |
172 | ## Contribution & Development
173 |
174 | See [CONTRIBUTING.md](https://github.com/developmentseed/rio-viz/blob/main/CONTRIBUTING.md)
175 |
176 | ## Authors
177 |
178 | Created by [Development Seed]()
179 |
180 | ## Changes
181 |
182 | See [CHANGES.md](https://github.com/developmentseed/rio-viz/blob/main/CHANGES.md).
183 |
184 | ## License
185 |
186 | See [LICENSE.txt](https://github.com/developmentseed/rio-viz/blob/main/LICENSE)
187 |
--------------------------------------------------------------------------------
/examples/Display_tiles_in_notebook.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "b6633f73",
6 | "metadata": {},
7 | "source": [
8 | "### Use rio-viz in Jupyter Notebook"
9 | ]
10 | },
11 | {
12 | "cell_type": "code",
13 | "execution_count": 5,
14 | "id": "55915667",
15 | "metadata": {},
16 | "outputs": [],
17 | "source": [
18 | "import time\n",
19 | "import httpx\n",
20 | "\n",
21 | "from folium import Map, TileLayer\n",
22 | "\n",
23 | "from rio_viz.app import Client"
24 | ]
25 | },
26 | {
27 | "cell_type": "code",
28 | "execution_count": 6,
29 | "id": "5c65b3d5",
30 | "metadata": {},
31 | "outputs": [
32 | {
33 | "name": "stdout",
34 | "output_type": "stream",
35 | "text": [
36 | "Client is alive: True\n"
37 | ]
38 | }
39 | ],
40 | "source": [
41 | "# Create rio-viz Client (using server-thread to launch backgroud task)\n",
42 | "client = Client(\"https://data.geo.admin.ch/ch.swisstopo.swissalti3d/swissalti3d_2019_2573-1085/swissalti3d_2019_2573-1085_0.5_2056_5728.tif\")\n",
43 | "\n",
44 | "# Gives some time for the server to setup\n",
45 | "time.sleep(1)\n",
46 | "\n",
47 | "# Check that client is running\n",
48 | "print(\"Client is alive: \", client.server.is_alive())"
49 | ]
50 | },
51 | {
52 | "cell_type": "code",
53 | "execution_count": 7,
54 | "id": "85e4c1e0",
55 | "metadata": {
56 | "scrolled": false
57 | },
58 | "outputs": [
59 | {
60 | "data": {
61 | "text/plain": [
62 | "{'bounds': [2573000.0, 1085000.0, 2574000.0, 1086000.0],\n",
63 | " 'crs': 'http://www.opengis.net/def/crs/EPSG/0/2056',\n",
64 | " 'band_metadata': [['b1',\n",
65 | " {'STATISTICS_COVARIANCES': '10685.98787505646',\n",
66 | " 'STATISTICS_EXCLUDEDVALUES': '-9999',\n",
67 | " 'STATISTICS_MAXIMUM': '2015.0944824219',\n",
68 | " 'STATISTICS_MEAN': '1754.471184271',\n",
69 | " 'STATISTICS_MINIMUM': '1615.8128662109',\n",
70 | " 'STATISTICS_SKIPFACTORX': '1',\n",
71 | " 'STATISTICS_SKIPFACTORY': '1',\n",
72 | " 'STATISTICS_STDDEV': '103.37305197708'}]],\n",
73 | " 'band_descriptions': [['b1', '']],\n",
74 | " 'dtype': 'float32',\n",
75 | " 'nodata_type': 'Nodata',\n",
76 | " 'colorinterp': ['gray'],\n",
77 | " 'scales': [1.0],\n",
78 | " 'offsets': [0.0],\n",
79 | " 'driver': 'GTiff',\n",
80 | " 'count': 1,\n",
81 | " 'width': 2000,\n",
82 | " 'height': 2000,\n",
83 | " 'overviews': [2, 4, 8],\n",
84 | " 'nodata_value': -9999.0}"
85 | ]
86 | },
87 | "execution_count": 7,
88 | "metadata": {},
89 | "output_type": "execute_result"
90 | }
91 | ],
92 | "source": [
93 | "# Fetch info\n",
94 | "httpx.get(f\"{client.endpoint}/info\").json()"
95 | ]
96 | },
97 | {
98 | "cell_type": "code",
99 | "execution_count": 8,
100 | "id": "4cc8c900",
101 | "metadata": {
102 | "scrolled": false
103 | },
104 | "outputs": [
105 | {
106 | "data": {
107 | "text/html": [
108 | "Make this Notebook Trusted to load map: File -> Trust Notebook
"
214 | ],
215 | "text/plain": [
216 | ""
217 | ]
218 | },
219 | "execution_count": 8,
220 | "metadata": {},
221 | "output_type": "execute_result"
222 | }
223 | ],
224 | "source": [
225 | "r = httpx.get(\n",
226 | " f\"{client.endpoint}/tilejson.json\",\n",
227 | " params = {\n",
228 | " \"rescale\": \"1600,2000\", # from the info endpoint\n",
229 | " \"colormap_name\": \"terrain\",\n",
230 | " }\n",
231 | ").json()\n",
232 | "\n",
233 | "bounds = r[\"bounds\"]\n",
234 | "m = Map(\n",
235 | " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n",
236 | " zoom_start=r[\"minzoom\"]\n",
237 | ")\n",
238 | "\n",
239 | "aod_layer = TileLayer(\n",
240 | " tiles=r[\"tiles\"][0],\n",
241 | " opacity=1,\n",
242 | " attr=\"Yo!!\"\n",
243 | ")\n",
244 | "aod_layer.add_to(m)\n",
245 | "m"
246 | ]
247 | },
248 | {
249 | "cell_type": "code",
250 | "execution_count": 9,
251 | "id": "54d674e9",
252 | "metadata": {},
253 | "outputs": [],
254 | "source": [
255 | "client.shutdown()"
256 | ]
257 | },
258 | {
259 | "cell_type": "code",
260 | "execution_count": null,
261 | "id": "adc13756",
262 | "metadata": {},
263 | "outputs": [],
264 | "source": []
265 | }
266 | ],
267 | "metadata": {
268 | "kernelspec": {
269 | "display_name": "Python 3 (ipykernel)",
270 | "language": "python",
271 | "name": "python3"
272 | },
273 | "language_info": {
274 | "codemirror_mode": {
275 | "name": "ipython",
276 | "version": 3
277 | },
278 | "file_extension": ".py",
279 | "mimetype": "text/x-python",
280 | "name": "python",
281 | "nbconvert_exporter": "python",
282 | "pygments_lexer": "ipython3",
283 | "version": "3.12.3"
284 | },
285 | "vscode": {
286 | "interpreter": {
287 | "hash": "b0fa6594d8f4cbf19f97940f81e996739fb7646882a419484c72d19e05852a7e"
288 | }
289 | }
290 | },
291 | "nbformat": 4,
292 | "nbformat_minor": 5
293 | }
294 |
--------------------------------------------------------------------------------
/examples/STAC_in_notebook.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "b6633f73",
6 | "metadata": {},
7 | "source": [
8 | "### Use rio-viz in Jupyter Notebook"
9 | ]
10 | },
11 | {
12 | "cell_type": "code",
13 | "execution_count": 9,
14 | "id": "55915667",
15 | "metadata": {},
16 | "outputs": [],
17 | "source": [
18 | "import time\n",
19 | "import httpx\n",
20 | "\n",
21 | "from ipyleaflet import Map, ScaleControl, FullScreenControl, SplitMapControl, TileLayer\n",
22 | "\n",
23 | "from rio_tiler.io import STACReader\n",
24 | "from rio_viz.app import Client"
25 | ]
26 | },
27 | {
28 | "cell_type": "code",
29 | "execution_count": 10,
30 | "id": "5c65b3d5",
31 | "metadata": {},
32 | "outputs": [
33 | {
34 | "name": "stdout",
35 | "output_type": "stream",
36 | "text": [
37 | "Client is alive: True\n"
38 | ]
39 | }
40 | ],
41 | "source": [
42 | "# Create rio-viz Client (using server-thread to launch backgroud task)\n",
43 | "client = Client(\n",
44 | " \"https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs/items/S2A_34SGA_20200318_0_L2A\",\n",
45 | " reader=STACReader,\n",
46 | " # By default STACReader min/max zoom is 0->24\n",
47 | " # Knowledge of the Sentinel-2 data tell us it's more 8->14\n",
48 | " minzoom=8,\n",
49 | " maxzoom=14,\n",
50 | ")\n",
51 | "\n",
52 | "# Gives some time for the server to setup\n",
53 | "time.sleep(1)\n",
54 | "\n",
55 | "# Check that client is running\n",
56 | "print(\"Client is alive: \", client.server.is_alive())"
57 | ]
58 | },
59 | {
60 | "cell_type": "code",
61 | "execution_count": 11,
62 | "id": "21137570",
63 | "metadata": {},
64 | "outputs": [
65 | {
66 | "data": {
67 | "text/plain": [
68 | "{'overview': {'bounds': [699960.0, 3490240.0, 809720.0, 3600000.0],\n",
69 | " 'crs': 'http://www.opengis.net/def/crs/EPSG/0/32634',\n",
70 | " 'band_metadata': [['b1', {}], ['b2', {}], ['b3', {}]],\n",
71 | " 'band_descriptions': [['b1', ''], ['b2', ''], ['b3', '']],\n",
72 | " 'dtype': 'uint8',\n",
73 | " 'nodata_type': 'Nodata',\n",
74 | " 'colorinterp': ['red', 'green', 'blue'],\n",
75 | " 'scales': [1.0, 1.0, 1.0],\n",
76 | " 'offsets': [0.0, 0.0, 0.0],\n",
77 | " 'driver': 'GTiff',\n",
78 | " 'count': 3,\n",
79 | " 'width': 343,\n",
80 | " 'height': 343,\n",
81 | " 'overviews': [2, 4],\n",
82 | " 'nodata_value': 0.0},\n",
83 | " 'B11': {'bounds': [699960.0, 3490200.0, 809760.0, 3600000.0],\n",
84 | " 'crs': 'http://www.opengis.net/def/crs/EPSG/0/32634',\n",
85 | " 'band_metadata': [['b1', {}]],\n",
86 | " 'band_descriptions': [['b1', '']],\n",
87 | " 'dtype': 'uint16',\n",
88 | " 'nodata_type': 'Nodata',\n",
89 | " 'colorinterp': ['gray'],\n",
90 | " 'scales': [1.0],\n",
91 | " 'offsets': [0.0],\n",
92 | " 'driver': 'GTiff',\n",
93 | " 'count': 1,\n",
94 | " 'width': 5490,\n",
95 | " 'height': 5490,\n",
96 | " 'overviews': [2, 4, 8],\n",
97 | " 'nodata_value': 0.0},\n",
98 | " 'B01': {'bounds': [699960.0, 3490200.0, 809760.0, 3600000.0],\n",
99 | " 'crs': 'http://www.opengis.net/def/crs/EPSG/0/32634',\n",
100 | " 'band_metadata': [['b1', {}]],\n",
101 | " 'band_descriptions': [['b1', '']],\n",
102 | " 'dtype': 'uint16',\n",
103 | " 'nodata_type': 'Nodata',\n",
104 | " 'colorinterp': ['gray'],\n",
105 | " 'scales': [1.0],\n",
106 | " 'offsets': [0.0],\n",
107 | " 'driver': 'GTiff',\n",
108 | " 'count': 1,\n",
109 | " 'width': 1830,\n",
110 | " 'height': 1830,\n",
111 | " 'overviews': [2, 4, 8],\n",
112 | " 'nodata_value': 0.0},\n",
113 | " 'B12': {'bounds': [699960.0, 3490200.0, 809760.0, 3600000.0],\n",
114 | " 'crs': 'http://www.opengis.net/def/crs/EPSG/0/32634',\n",
115 | " 'band_metadata': [['b1', {}]],\n",
116 | " 'band_descriptions': [['b1', '']],\n",
117 | " 'dtype': 'uint16',\n",
118 | " 'nodata_type': 'Nodata',\n",
119 | " 'colorinterp': ['gray'],\n",
120 | " 'scales': [1.0],\n",
121 | " 'offsets': [0.0],\n",
122 | " 'driver': 'GTiff',\n",
123 | " 'count': 1,\n",
124 | " 'width': 5490,\n",
125 | " 'height': 5490,\n",
126 | " 'overviews': [2, 4, 8, 16],\n",
127 | " 'nodata_value': 0.0},\n",
128 | " 'B02': {'bounds': [699960.0, 3490200.0, 809760.0, 3600000.0],\n",
129 | " 'crs': 'http://www.opengis.net/def/crs/EPSG/0/32634',\n",
130 | " 'band_metadata': [['b1', {}]],\n",
131 | " 'band_descriptions': [['b1', '']],\n",
132 | " 'dtype': 'uint16',\n",
133 | " 'nodata_type': 'Nodata',\n",
134 | " 'colorinterp': ['gray'],\n",
135 | " 'scales': [1.0],\n",
136 | " 'offsets': [0.0],\n",
137 | " 'driver': 'GTiff',\n",
138 | " 'count': 1,\n",
139 | " 'width': 10980,\n",
140 | " 'height': 10980,\n",
141 | " 'overviews': [2, 4, 8, 16],\n",
142 | " 'nodata_value': 0.0},\n",
143 | " 'B03': {'bounds': [699960.0, 3490200.0, 809760.0, 3600000.0],\n",
144 | " 'crs': 'http://www.opengis.net/def/crs/EPSG/0/32634',\n",
145 | " 'band_metadata': [['b1', {}]],\n",
146 | " 'band_descriptions': [['b1', '']],\n",
147 | " 'dtype': 'uint16',\n",
148 | " 'nodata_type': 'Nodata',\n",
149 | " 'colorinterp': ['gray'],\n",
150 | " 'scales': [1.0],\n",
151 | " 'offsets': [0.0],\n",
152 | " 'driver': 'GTiff',\n",
153 | " 'count': 1,\n",
154 | " 'width': 10980,\n",
155 | " 'height': 10980,\n",
156 | " 'overviews': [2, 4, 8, 16],\n",
157 | " 'nodata_value': 0.0},\n",
158 | " 'B04': {'bounds': [699960.0, 3490200.0, 809760.0, 3600000.0],\n",
159 | " 'crs': 'http://www.opengis.net/def/crs/EPSG/0/32634',\n",
160 | " 'band_metadata': [['b1', {}]],\n",
161 | " 'band_descriptions': [['b1', '']],\n",
162 | " 'dtype': 'uint16',\n",
163 | " 'nodata_type': 'Nodata',\n",
164 | " 'colorinterp': ['gray'],\n",
165 | " 'scales': [1.0],\n",
166 | " 'offsets': [0.0],\n",
167 | " 'driver': 'GTiff',\n",
168 | " 'count': 1,\n",
169 | " 'width': 10980,\n",
170 | " 'height': 10980,\n",
171 | " 'overviews': [2, 4, 8, 16],\n",
172 | " 'nodata_value': 0.0},\n",
173 | " 'AOT': {'bounds': [699960.0, 3490200.0, 809760.0, 3600000.0],\n",
174 | " 'crs': 'http://www.opengis.net/def/crs/EPSG/0/32634',\n",
175 | " 'band_metadata': [['b1', {}]],\n",
176 | " 'band_descriptions': [['b1', '']],\n",
177 | " 'dtype': 'uint16',\n",
178 | " 'nodata_type': 'Nodata',\n",
179 | " 'colorinterp': ['gray'],\n",
180 | " 'scales': [1.0],\n",
181 | " 'offsets': [0.0],\n",
182 | " 'driver': 'GTiff',\n",
183 | " 'count': 1,\n",
184 | " 'width': 1830,\n",
185 | " 'height': 1830,\n",
186 | " 'overviews': [2, 4, 8],\n",
187 | " 'nodata_value': 0.0},\n",
188 | " 'B05': {'bounds': [699960.0, 3490200.0, 809760.0, 3600000.0],\n",
189 | " 'crs': 'http://www.opengis.net/def/crs/EPSG/0/32634',\n",
190 | " 'band_metadata': [['b1', {}]],\n",
191 | " 'band_descriptions': [['b1', '']],\n",
192 | " 'dtype': 'uint16',\n",
193 | " 'nodata_type': 'Nodata',\n",
194 | " 'colorinterp': ['gray'],\n",
195 | " 'scales': [1.0],\n",
196 | " 'offsets': [0.0],\n",
197 | " 'driver': 'GTiff',\n",
198 | " 'count': 1,\n",
199 | " 'width': 5490,\n",
200 | " 'height': 5490,\n",
201 | " 'overviews': [2, 4, 8, 16],\n",
202 | " 'nodata_value': 0.0},\n",
203 | " 'B06': {'bounds': [699960.0, 3490200.0, 809760.0, 3600000.0],\n",
204 | " 'crs': 'http://www.opengis.net/def/crs/EPSG/0/32634',\n",
205 | " 'band_metadata': [['b1', {}]],\n",
206 | " 'band_descriptions': [['b1', '']],\n",
207 | " 'dtype': 'uint16',\n",
208 | " 'nodata_type': 'Nodata',\n",
209 | " 'colorinterp': ['gray'],\n",
210 | " 'scales': [1.0],\n",
211 | " 'offsets': [0.0],\n",
212 | " 'driver': 'GTiff',\n",
213 | " 'count': 1,\n",
214 | " 'width': 5490,\n",
215 | " 'height': 5490,\n",
216 | " 'overviews': [2, 4, 8, 16],\n",
217 | " 'nodata_value': 0.0},\n",
218 | " 'B07': {'bounds': [699960.0, 3490200.0, 809760.0, 3600000.0],\n",
219 | " 'crs': 'http://www.opengis.net/def/crs/EPSG/0/32634',\n",
220 | " 'band_metadata': [['b1', {}]],\n",
221 | " 'band_descriptions': [['b1', '']],\n",
222 | " 'dtype': 'uint16',\n",
223 | " 'nodata_type': 'Nodata',\n",
224 | " 'colorinterp': ['gray'],\n",
225 | " 'scales': [1.0],\n",
226 | " 'offsets': [0.0],\n",
227 | " 'driver': 'GTiff',\n",
228 | " 'count': 1,\n",
229 | " 'width': 5490,\n",
230 | " 'height': 5490,\n",
231 | " 'overviews': [2, 4, 8, 16],\n",
232 | " 'nodata_value': 0.0},\n",
233 | " 'B08': {'bounds': [699960.0, 3490200.0, 809760.0, 3600000.0],\n",
234 | " 'crs': 'http://www.opengis.net/def/crs/EPSG/0/32634',\n",
235 | " 'band_metadata': [['b1', {}]],\n",
236 | " 'band_descriptions': [['b1', '']],\n",
237 | " 'dtype': 'uint16',\n",
238 | " 'nodata_type': 'Nodata',\n",
239 | " 'colorinterp': ['gray'],\n",
240 | " 'scales': [1.0],\n",
241 | " 'offsets': [0.0],\n",
242 | " 'driver': 'GTiff',\n",
243 | " 'count': 1,\n",
244 | " 'width': 10980,\n",
245 | " 'height': 10980,\n",
246 | " 'overviews': [2, 4, 8, 16],\n",
247 | " 'nodata_value': 0.0},\n",
248 | " 'B8A': {'bounds': [699960.0, 3490200.0, 809760.0, 3600000.0],\n",
249 | " 'crs': 'http://www.opengis.net/def/crs/EPSG/0/32634',\n",
250 | " 'band_metadata': [['b1', {}]],\n",
251 | " 'band_descriptions': [['b1', '']],\n",
252 | " 'dtype': 'uint16',\n",
253 | " 'nodata_type': 'Nodata',\n",
254 | " 'colorinterp': ['gray'],\n",
255 | " 'scales': [1.0],\n",
256 | " 'offsets': [0.0],\n",
257 | " 'driver': 'GTiff',\n",
258 | " 'count': 1,\n",
259 | " 'width': 5490,\n",
260 | " 'height': 5490,\n",
261 | " 'overviews': [2, 4, 8, 16],\n",
262 | " 'nodata_value': 0.0},\n",
263 | " 'B09': {'bounds': [699960.0, 3490200.0, 809760.0, 3600000.0],\n",
264 | " 'crs': 'http://www.opengis.net/def/crs/EPSG/0/32634',\n",
265 | " 'band_metadata': [['b1', {}]],\n",
266 | " 'band_descriptions': [['b1', '']],\n",
267 | " 'dtype': 'uint16',\n",
268 | " 'nodata_type': 'Nodata',\n",
269 | " 'colorinterp': ['gray'],\n",
270 | " 'scales': [1.0],\n",
271 | " 'offsets': [0.0],\n",
272 | " 'driver': 'GTiff',\n",
273 | " 'count': 1,\n",
274 | " 'width': 1830,\n",
275 | " 'height': 1830,\n",
276 | " 'overviews': [2, 4, 8],\n",
277 | " 'nodata_value': 0.0},\n",
278 | " 'WVP': {'bounds': [699960.0, 3490200.0, 809760.0, 3600000.0],\n",
279 | " 'crs': 'http://www.opengis.net/def/crs/EPSG/0/32634',\n",
280 | " 'band_metadata': [['b1', {}]],\n",
281 | " 'band_descriptions': [['b1', '']],\n",
282 | " 'dtype': 'uint16',\n",
283 | " 'nodata_type': 'Nodata',\n",
284 | " 'colorinterp': ['gray'],\n",
285 | " 'scales': [1.0],\n",
286 | " 'offsets': [0.0],\n",
287 | " 'driver': 'GTiff',\n",
288 | " 'count': 1,\n",
289 | " 'width': 10980,\n",
290 | " 'height': 10980,\n",
291 | " 'overviews': [2, 4, 8, 16],\n",
292 | " 'nodata_value': 0.0},\n",
293 | " 'visual': {'bounds': [699960.0, 3490200.0, 809760.0, 3600000.0],\n",
294 | " 'crs': 'http://www.opengis.net/def/crs/EPSG/0/32634',\n",
295 | " 'band_metadata': [['b1', {}], ['b2', {}], ['b3', {}]],\n",
296 | " 'band_descriptions': [['b1', ''], ['b2', ''], ['b3', '']],\n",
297 | " 'dtype': 'uint8',\n",
298 | " 'nodata_type': 'Nodata',\n",
299 | " 'colorinterp': ['red', 'green', 'blue'],\n",
300 | " 'scales': [1.0, 1.0, 1.0],\n",
301 | " 'offsets': [0.0, 0.0, 0.0],\n",
302 | " 'driver': 'GTiff',\n",
303 | " 'count': 3,\n",
304 | " 'width': 10980,\n",
305 | " 'height': 10980,\n",
306 | " 'overviews': [2, 4, 8, 16],\n",
307 | " 'nodata_value': 0.0},\n",
308 | " 'SCL': {'bounds': [699960.0, 3490200.0, 809760.0, 3600000.0],\n",
309 | " 'crs': 'http://www.opengis.net/def/crs/EPSG/0/32634',\n",
310 | " 'band_metadata': [['b1', {}]],\n",
311 | " 'band_descriptions': [['b1', '']],\n",
312 | " 'dtype': 'uint8',\n",
313 | " 'nodata_type': 'Nodata',\n",
314 | " 'colorinterp': ['gray'],\n",
315 | " 'scales': [1.0],\n",
316 | " 'offsets': [0.0],\n",
317 | " 'driver': 'GTiff',\n",
318 | " 'count': 1,\n",
319 | " 'width': 5490,\n",
320 | " 'height': 5490,\n",
321 | " 'overviews': [2, 4, 8, 16],\n",
322 | " 'nodata_value': 0.0}}"
323 | ]
324 | },
325 | "execution_count": 11,
326 | "metadata": {},
327 | "output_type": "execute_result"
328 | }
329 | ],
330 | "source": [
331 | "# Fetch info\n",
332 | "httpx.get(f\"{client.endpoint}/info\").json()"
333 | ]
334 | },
335 | {
336 | "cell_type": "code",
337 | "execution_count": 14,
338 | "id": "4070b77d",
339 | "metadata": {
340 | "scrolled": false
341 | },
342 | "outputs": [
343 | {
344 | "data": {
345 | "application/vnd.jupyter.widget-view+json": {
346 | "model_id": "17bef26f130a48c1ab65e1cc9d4adcfa",
347 | "version_major": 2,
348 | "version_minor": 0
349 | },
350 | "text/plain": [
351 | "Map(center=[32.00833055925221, 23.794854319372455], controls=(ZoomControl(options=['position', 'zoom_in_text',…"
352 | ]
353 | },
354 | "execution_count": 14,
355 | "metadata": {},
356 | "output_type": "execute_result"
357 | }
358 | ],
359 | "source": [
360 | "tilejson = httpx.get(\n",
361 | " f\"{client.endpoint}/tilejson.json\",\n",
362 | " params = {\n",
363 | " \"assets\": [\"B04\", \"B03\", \"B02\"],\n",
364 | " \"rescale\": \"0,10000\"\n",
365 | " }\n",
366 | ").json()\n",
367 | "\n",
368 | "bounds = tilejson[\"bounds\"]\n",
369 | "\n",
370 | "layer = TileLayer(\n",
371 | " url=tilejson[\"tiles\"][0],\n",
372 | " min_zoom=tilejson[\"minzoom\"],\n",
373 | " max_zoom=tilejson[\"maxzoom\"],\n",
374 | " bounds=((bounds[0],bounds[2]), (bounds[1], bounds[3])),\n",
375 | ")\n",
376 | "\n",
377 | "\n",
378 | "# Make the ipyleaflet map\n",
379 | "m = Map(center=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom=client.minzoom)\n",
380 | "m.add_layer(layer)\n",
381 | "m"
382 | ]
383 | },
384 | {
385 | "cell_type": "code",
386 | "execution_count": 13,
387 | "id": "85e4c1e0",
388 | "metadata": {
389 | "scrolled": false
390 | },
391 | "outputs": [
392 | {
393 | "data": {
394 | "application/vnd.jupyter.widget-view+json": {
395 | "model_id": "8e49ca8686664e7aae06f10173013f7a",
396 | "version_major": 2,
397 | "version_minor": 0
398 | },
399 | "text/plain": [
400 | "Map(center=[32.00833055925221, 23.794854319372455], controls=(ZoomControl(options=['position', 'zoom_in_text',…"
401 | ]
402 | },
403 | "execution_count": 13,
404 | "metadata": {},
405 | "output_type": "execute_result"
406 | }
407 | ],
408 | "source": [
409 | "left_tilejson = httpx.get(\n",
410 | " f\"{client.endpoint}/tilejson.json\",\n",
411 | " params = {\n",
412 | " \"assets\": [\"B04\", \"B03\", \"B02\"],\n",
413 | " \"rescale\": \"0,10000\"\n",
414 | " }\n",
415 | ").json()\n",
416 | "\n",
417 | "right_tilejson = httpx.get(\n",
418 | " f\"{client.endpoint}/tilejson.json\",\n",
419 | " params = {\n",
420 | " \"assets\": [\"B05\", \"B04\", \"B03\"],\n",
421 | " \"rescale\": \"0,10000\"\n",
422 | " }\n",
423 | ").json()\n",
424 | "\n",
425 | "bounds = tilejson[\"bounds\"]\n",
426 | "\n",
427 | "left = TileLayer(\n",
428 | " url=left_tilejson[\"tiles\"][0],\n",
429 | " min_zoom=left_tilejson[\"minzoom\"],\n",
430 | " max_zoom=left_tilejson[\"maxzoom\"],\n",
431 | " bounds=((bounds[0],bounds[2]), (bounds[1], bounds[3])),\n",
432 | ")\n",
433 | "\n",
434 | "right = TileLayer(\n",
435 | " url=right_tilejson[\"tiles\"][0],\n",
436 | " min_zoom=right_tilejson[\"minzoom\"],\n",
437 | " max_zoom=right_tilejson[\"maxzoom\"],\n",
438 | " bounds=((bounds[0],bounds[2]), (bounds[1], bounds[3])),\n",
439 | ")\n",
440 | "\n",
441 | "# Make the ipyleaflet map\n",
442 | "m = Map(center=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom=client.minzoom)\n",
443 | "control = SplitMapControl(left_layer=left, right_layer=right)\n",
444 | "\n",
445 | "m.add_control(control)\n",
446 | "m.add_control(ScaleControl(position='bottomleft'))\n",
447 | "m.add_control(FullScreenControl())\n",
448 | "m"
449 | ]
450 | },
451 | {
452 | "cell_type": "code",
453 | "execution_count": null,
454 | "id": "2069c2f2",
455 | "metadata": {},
456 | "outputs": [],
457 | "source": [
458 | "client.shutdown()"
459 | ]
460 | }
461 | ],
462 | "metadata": {
463 | "kernelspec": {
464 | "display_name": "Python 3 (ipykernel)",
465 | "language": "python",
466 | "name": "python3"
467 | },
468 | "language_info": {
469 | "codemirror_mode": {
470 | "name": "ipython",
471 | "version": 3
472 | },
473 | "file_extension": ".py",
474 | "mimetype": "text/x-python",
475 | "name": "python",
476 | "nbconvert_exporter": "python",
477 | "pygments_lexer": "ipython3",
478 | "version": "3.12.3"
479 | },
480 | "vscode": {
481 | "interpreter": {
482 | "hash": "b0fa6594d8f4cbf19f97940f81e996739fb7646882a419484c72d19e05852a7e"
483 | }
484 | }
485 | },
486 | "nbformat": 4,
487 | "nbformat_minor": 5
488 | }
489 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "rio-viz"
3 | description = "Visualize Cloud Optimized GeoTIFF in browser"
4 | readme = "README.md"
5 | requires-python = ">=3.9"
6 | license = {file = "LICENSE"}
7 | authors = [
8 | {name = "Vincent Sarago", email = "vincent@developmentseed.com"},
9 | ]
10 | classifiers = [
11 | "Intended Audience :: Information Technology",
12 | "Intended Audience :: Science/Research",
13 | "License :: OSI Approved :: BSD License",
14 | "Programming Language :: Python :: 3.9",
15 | "Programming Language :: Python :: 3.10",
16 | "Programming Language :: Python :: 3.11",
17 | "Programming Language :: Python :: 3.12",
18 | "Programming Language :: Python :: 3.13",
19 | "Topic :: Scientific/Engineering :: GIS",
20 | ]
21 | dynamic = ["version"]
22 | dependencies = [
23 | "braceexpand",
24 | "rio-cogeo>=5.0",
25 | "titiler.core>=0.20.0,<0.22",
26 | "starlette-cramjam>=0.4,<0.5",
27 | "uvicorn",
28 | "server-thread>=0.2.0",
29 | ]
30 |
31 | [project.optional-dependencies]
32 | mvt = [
33 | "rio-tiler-mvt>=0.2,<0.3",
34 | ]
35 | test = [
36 | "pytest",
37 | "pytest-cov",
38 | "pytest-asyncio",
39 | "requests",
40 | ]
41 | dev = [
42 | "pre-commit",
43 | ]
44 |
45 | [project.urls]
46 | Source = "https://github.com/developmentseed/rio-viz"
47 |
48 | [project.entry-points."rasterio.rio_plugins"]
49 | viz = "rio_viz.scripts.cli:viz"
50 |
51 | [build-system]
52 | requires = ["hatchling"]
53 | build-backend = "hatchling.build"
54 |
55 | [tool.hatch.version]
56 | path = "rio_viz/__init__.py"
57 |
58 | [tool.hatch.build.targets.sdist]
59 | exclude = [
60 | "/tests",
61 | ".ruff_cache/",
62 | "examples/",
63 | ".github",
64 | ".history",
65 | ".bumpversion.cfg",
66 | ".gitignore",
67 | "Dockerfile",
68 | ".pre-commit-config.yaml",
69 | "CHANGES.md",
70 | "CONTRIBUTING.md",
71 | ]
72 |
73 | [tool.isort]
74 | profile = "black"
75 | known_first_party = ["rio_viz"]
76 | known_third_party = ["rasterio", "rio_tiler", "morecantile", "titiler"]
77 | forced_separate = ["titiler"]
78 | default_section = "THIRDPARTY"
79 |
80 | [tool.mypy]
81 | no_strict_optional = true
82 |
83 | [tool.ruff]
84 | line-length = 90
85 |
86 | [tool.ruff.lint]
87 | select = [
88 | "D1", # pydocstyle errors
89 | "E", # pycodestyle errors
90 | "W", # pycodestyle warnings
91 | "F", # flake8
92 | "C", # flake8-comprehensions
93 | "B", # flake8-bugbear
94 | ]
95 | ignore = [
96 | "E501", # line too long, handled by black
97 | "B008", # do not perform function calls in argument defaults
98 | "B905", # ignore zip() without an explicit strict= parameter, only support with python >3.10
99 | "B028",
100 | ]
101 |
102 | [tool.ruff.lint.mccabe]
103 | max-complexity = 12
104 |
105 | [tool.bumpversion]
106 | current_version = "0.14.0"
107 | search = "{current_version}"
108 | replace = "{new_version}"
109 | regex = false
110 | tag = true
111 | commit = true
112 | tag_name = "{new_version}"
113 |
114 | [[tool.bumpversion.files]]
115 | filename = "rio_viz/__init__.py"
116 | search = '__version__ = "{current_version}"'
117 | replace = '__version__ = "{new_version}"'
118 |
--------------------------------------------------------------------------------
/rio_viz/__init__.py:
--------------------------------------------------------------------------------
1 | """rio_viz."""
2 |
3 | __version__ = "0.14.0"
4 |
5 | from rio_viz.app import viz # noqa
6 |
--------------------------------------------------------------------------------
/rio_viz/app.py:
--------------------------------------------------------------------------------
1 | """rio_viz app."""
2 |
3 | import urllib.parse
4 | from typing import Any, Dict, List, Literal, Optional, Tuple, Type, Union
5 |
6 | import attr
7 | import jinja2
8 | import rasterio
9 | import uvicorn
10 | from fastapi import APIRouter, Depends, FastAPI, HTTPException, Path, Query
11 | from geojson_pydantic.features import Feature
12 | from geojson_pydantic.geometries import MultiPolygon, Polygon
13 | from rio_tiler.constants import WGS84_CRS
14 | from rio_tiler.io import BaseReader, MultiBandReader, MultiBaseReader, Reader
15 | from rio_tiler.models import BandStatistics, Info
16 | from server_thread import ServerManager, ServerThread
17 | from starlette.middleware.cors import CORSMiddleware
18 | from starlette.requests import Request
19 | from starlette.responses import HTMLResponse, Response
20 | from starlette.templating import Jinja2Templates
21 | from starlette_cramjam.middleware import CompressionMiddleware
22 | from typing_extensions import Annotated
23 |
24 | from rio_viz.resources.enums import RasterFormat, VectorTileFormat
25 |
26 | from titiler.core.algorithm import algorithms as available_algorithms
27 | from titiler.core.dependencies import (
28 | AssetsBidxExprParamsOptional,
29 | AssetsBidxParams,
30 | AssetsParams,
31 | BandsExprParamsOptional,
32 | BandsParams,
33 | BidxExprParams,
34 | ColorMapParams,
35 | CoordCRSParams,
36 | CRSParams,
37 | DatasetParams,
38 | DefaultDependency,
39 | DstCRSParams,
40 | HistogramParams,
41 | ImageRenderingParams,
42 | PartFeatureParams,
43 | PreviewParams,
44 | StatisticsParams,
45 | TileParams,
46 | )
47 | from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers
48 | from titiler.core.middleware import CacheControlMiddleware
49 | from titiler.core.models.mapbox import TileJSON
50 | from titiler.core.models.responses import (
51 | InfoGeoJSON,
52 | MultiBaseInfo,
53 | MultiBaseInfoGeoJSON,
54 | )
55 | from titiler.core.resources.responses import GeoJSONResponse, JSONResponse, XMLResponse
56 | from titiler.core.utils import render_image
57 |
58 | try:
59 | from rio_tiler_mvt import pixels_encoder # noqa
60 |
61 | has_mvt = True
62 | except ModuleNotFoundError:
63 | has_mvt = False
64 | pixels_encoder = None
65 |
66 | jinja2_env = jinja2.Environment(
67 | loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")])
68 | )
69 | templates = Jinja2Templates(env=jinja2_env)
70 |
71 | TileFormat = Union[RasterFormat, VectorTileFormat]
72 |
73 |
74 | @attr.s
75 | class viz:
76 | """Creates a very minimal slippy map tile server using fastAPI + Uvicorn."""
77 |
78 | src_path: str = attr.ib()
79 | reader: Union[Type[BaseReader], Type[MultiBandReader], Type[MultiBaseReader]] = (
80 | attr.ib(default=Reader)
81 | )
82 | reader_params: Dict = attr.ib(factory=dict)
83 | app: FastAPI = attr.ib(factory=FastAPI)
84 |
85 | port: int = attr.ib(default=8080)
86 | host: str = attr.ib(default="127.0.0.1")
87 | config: Dict = attr.ib(factory=dict)
88 |
89 | minzoom: Optional[int] = attr.ib(default=None)
90 | maxzoom: Optional[int] = attr.ib(default=None)
91 | bounds: Optional[Tuple[float, float, float, float]] = attr.ib(default=None)
92 |
93 | layers: Optional[List[str]] = attr.ib(default=None)
94 | nodata: Optional[Union[str, int, float]] = attr.ib(default=None)
95 |
96 | geojson: Optional[Dict] = attr.ib(default=None)
97 |
98 | # cog / bands / assets
99 | reader_type: str = attr.ib(init=False)
100 |
101 | router: Optional[APIRouter] = attr.ib(init=False)
102 |
103 | statistics_dependency: Type[DefaultDependency] = attr.ib(init=False)
104 | layer_dependency: Type[DefaultDependency] = attr.ib(init=False)
105 |
106 | def __attrs_post_init__(self):
107 | """Update App."""
108 | self.router = APIRouter()
109 |
110 | if issubclass(self.reader, (MultiBandReader)):
111 | self.reader_type = "bands"
112 | elif issubclass(self.reader, (MultiBaseReader)):
113 | self.reader_type = "assets"
114 | else:
115 | self.reader_type = "cog"
116 |
117 | if self.reader_type == "cog":
118 | # For simple BaseReader (e.g Reader) we don't add more dependencies.
119 | self.info_dependency = DefaultDependency
120 | self.statistics_dependency = BidxExprParams
121 | self.layer_dependency = BidxExprParams
122 |
123 | elif self.reader_type == "bands":
124 | self.info_dependency = BandsParams
125 | self.statistics_dependency = BandsExprParamsOptional
126 | self.layer_dependency = BandsExprParamsOptional
127 |
128 | elif self.reader_type == "assets":
129 | self.info_dependency = AssetsParams
130 | self.statistics_dependency = AssetsBidxParams
131 | self.layer_dependency = AssetsBidxExprParamsOptional
132 |
133 | self.register_middleware()
134 | self.register_routes()
135 | self.app.include_router(self.router)
136 | add_exception_handlers(self.app, DEFAULT_STATUS_CODES)
137 |
138 | def register_middleware(self):
139 | """Register Middleware to the FastAPI app."""
140 | self.app.add_middleware(
141 | CompressionMiddleware,
142 | minimum_size=0,
143 | exclude_mediatype={
144 | "image/jpeg",
145 | "image/jpg",
146 | "image/png",
147 | "image/jp2",
148 | "image/webp",
149 | },
150 | )
151 | self.app.add_middleware(
152 | CORSMiddleware,
153 | allow_origins=["*"],
154 | allow_credentials=True,
155 | allow_methods=["GET"],
156 | allow_headers=["*"],
157 | )
158 | self.app.add_middleware(CacheControlMiddleware, cachecontrol="no-cache")
159 |
160 | def _update_params(self, src_dst, options: Type[DefaultDependency]):
161 | """Create Reader options."""
162 | if not getattr(options, "expression", None):
163 | if self.reader_type == "bands":
164 | # get default bands from self.layers or reader.bands
165 | bands = self.layers or getattr(src_dst, "bands", None)
166 | # check if bands is not in options and overwrite
167 | if bands and not getattr(options, "bands", None):
168 | options.bands = bands
169 |
170 | if self.reader_type == "assets":
171 | # get default assets from self.layers or reader.assets
172 | assets = self.layers or getattr(src_dst, "assets", None)
173 | # check if assets is not in options and overwrite
174 | if assets and not getattr(options, "assets", None):
175 | options.assets = assets
176 |
177 | def register_routes(self): # noqa
178 | """Register routes to the FastAPI app."""
179 | img_media_types = {
180 | "image/png": {},
181 | "image/jpeg": {},
182 | "image/webp": {},
183 | "image/jp2": {},
184 | "image/tiff; application=geotiff": {},
185 | "application/x-binary": {},
186 | }
187 | mvt_media_types = {
188 | "application/x-binary": {},
189 | "application/x-protobuf": {},
190 | }
191 |
192 | @self.router.get(
193 | "/info",
194 | # for MultiBaseReader the output in `Dict[str, Info]`
195 | response_model=MultiBaseInfo if self.reader_type == "assets" else Info,
196 | response_model_exclude_none=True,
197 | response_class=JSONResponse,
198 | responses={200: {"description": "Return the info of the COG."}},
199 | tags=["API"],
200 | )
201 | def info(params=Depends(self.info_dependency)):
202 | """Handle /info requests."""
203 | with self.reader(self.src_path, **self.reader_params) as src_dst:
204 | # Adapt options for each reader type
205 | self._update_params(src_dst, params)
206 | return src_dst.info(**params.as_dict())
207 |
208 | @self.router.get(
209 | "/info.geojson",
210 | # for MultiBaseReader the output in `Dict[str, Info]`
211 | response_model=MultiBaseInfoGeoJSON
212 | if self.reader_type == "assets"
213 | else InfoGeoJSON,
214 | response_model_exclude_none=True,
215 | response_class=GeoJSONResponse,
216 | responses={
217 | 200: {
218 | "content": {"application/geo+json": {}},
219 | "description": "Return dataset's basic info as a GeoJSON feature.",
220 | }
221 | },
222 | tags=["API"],
223 | )
224 | def info_geojson(
225 | params=Depends(self.info_dependency),
226 | crs=Depends(CRSParams),
227 | ):
228 | """Handle /info requests."""
229 | with self.reader(self.src_path, **self.reader_params) as src_dst:
230 | bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS)
231 | if bounds[0] > bounds[2]:
232 | pl = Polygon.from_bounds(-180, bounds[1], bounds[2], bounds[3])
233 | pr = Polygon.from_bounds(bounds[0], bounds[1], 180, bounds[3])
234 | geometry = MultiPolygon(
235 | type="MultiPolygon",
236 | coordinates=[pl.coordinates, pr.coordinates],
237 | )
238 | else:
239 | geometry = Polygon.from_bounds(*bounds)
240 |
241 | # Adapt options for each reader type
242 | self._update_params(src_dst, params)
243 | return Feature(
244 | type="Feature",
245 | bbox=bounds,
246 | geometry=geometry,
247 | properties=src_dst.info(**params.as_dict()),
248 | )
249 |
250 | @self.router.get(
251 | "/statistics",
252 | # for MultiBaseReader the output in `Dict[str, Dict[str, ImageStatistics]]`
253 | response_model=Dict[str, Dict[str, BandStatistics]]
254 | if self.reader_type == "assets"
255 | else Dict[str, BandStatistics],
256 | response_model_exclude_none=True,
257 | response_class=JSONResponse,
258 | responses={200: {"description": "Return the statistics of the COG."}},
259 | tags=["API"],
260 | )
261 | def statistics(
262 | layer_params=Depends(self.statistics_dependency),
263 | image_params: PreviewParams = Depends(),
264 | dataset_params: DatasetParams = Depends(),
265 | stats_params: StatisticsParams = Depends(),
266 | histogram_params: HistogramParams = Depends(),
267 | ):
268 | """Handle /stats requests."""
269 | with self.reader(self.src_path, **self.reader_params) as src_dst:
270 | if self.nodata is not None and dataset_params.nodata is None:
271 | dataset_params.nodata = self.nodata
272 |
273 | # Adapt options for each reader type
274 | self._update_params(src_dst, layer_params)
275 |
276 | return src_dst.statistics(
277 | **layer_params.as_dict(),
278 | **dataset_params.as_dict(),
279 | **image_params.as_dict(),
280 | **stats_params.as_dict(),
281 | hist_options=histogram_params.as_dict(),
282 | )
283 |
284 | @self.router.get(
285 | "/point",
286 | responses={200: {"description": "Return a point value."}},
287 | response_class=JSONResponse,
288 | tags=["API"],
289 | )
290 | def point(
291 | coordinates: Annotated[
292 | str,
293 | Query(description="Coma (',') delimited lon,lat coordinates"),
294 | ],
295 | layer_params=Depends(self.layer_dependency),
296 | dataset_params: DatasetParams = Depends(),
297 | ):
298 | """Handle /point requests."""
299 | lon, lat = list(map(float, coordinates.split(",")))
300 | with self.reader(self.src_path, **self.reader_params) as src_dst: # type: ignore
301 | if self.nodata is not None and dataset_params.nodata is None:
302 | dataset_params.nodata = self.nodata
303 |
304 | # Adapt options for each reader type
305 | self._update_params(src_dst, layer_params)
306 |
307 | pts = src_dst.point(
308 | lon,
309 | lat,
310 | **layer_params.as_dict(),
311 | **dataset_params.as_dict(),
312 | )
313 |
314 | return {
315 | "coordinates": [lon, lat],
316 | "values": pts.array.tolist(),
317 | "band_names": pts.band_names,
318 | }
319 |
320 | preview_params = {
321 | "responses": {
322 | 200: {"content": img_media_types, "description": "Return a preview."}
323 | },
324 | "response_class": Response,
325 | "description": "Return a preview.",
326 | }
327 |
328 | @self.router.get("/preview", **preview_params, tags=["API"])
329 | @self.router.get("/preview.{format}", **preview_params, tags=["API"])
330 | def preview(
331 | format: Optional[RasterFormat] = None,
332 | layer_params=Depends(self.layer_dependency),
333 | img_params: PreviewParams = Depends(),
334 | dataset_params: DatasetParams = Depends(),
335 | render_params: ImageRenderingParams = Depends(),
336 | colormap: ColorMapParams = Depends(),
337 | post_process=Depends(available_algorithms.dependency),
338 | ):
339 | """Handle /preview requests."""
340 | with self.reader(self.src_path, **self.reader_params) as src_dst: # type: ignore
341 | if self.nodata is not None and dataset_params.nodata is None:
342 | dataset_params.nodata = self.nodata
343 |
344 | # Adapt options for each reader type
345 | self._update_params(src_dst, layer_params)
346 |
347 | image = src_dst.preview(
348 | **layer_params.as_dict(),
349 | **dataset_params.as_dict(),
350 | **img_params.as_dict(),
351 | )
352 | dst_colormap = getattr(src_dst, "colormap", None)
353 |
354 | if post_process:
355 | image = post_process(image)
356 |
357 | content, media_type = render_image(
358 | image,
359 | output_format=format,
360 | colormap=colormap or dst_colormap,
361 | **render_params.as_dict(),
362 | )
363 |
364 | return Response(content, media_type=media_type)
365 |
366 | part_params = {
367 | "responses": {
368 | 200: {
369 | "content": img_media_types,
370 | "description": "Return a part of a dataset.",
371 | }
372 | },
373 | "response_class": Response,
374 | "description": "Return a part of a dataset.",
375 | }
376 |
377 | @self.router.get(
378 | "/bbox/{minx},{miny},{maxx},{maxy}.{format}",
379 | **part_params,
380 | tags=["API"],
381 | )
382 | @self.router.get(
383 | "/bbox/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}",
384 | **part_params,
385 | tags=["API"],
386 | )
387 | def part(
388 | minx: Annotated[float, Path(description="Bounding box min X")],
389 | miny: Annotated[float, Path(description="Bounding box min Y")],
390 | maxx: Annotated[float, Path(description="Bounding box max X")],
391 | maxy: Annotated[float, Path(description="Bounding box max Y")],
392 | format: Annotated[
393 | RasterFormat,
394 | "Output image type.",
395 | ] = RasterFormat.png,
396 | layer_params=Depends(self.layer_dependency),
397 | img_params: PartFeatureParams = Depends(),
398 | dataset_params: DatasetParams = Depends(),
399 | render_params: ImageRenderingParams = Depends(),
400 | colormap: ColorMapParams = Depends(),
401 | dst_crs=Depends(DstCRSParams),
402 | coord_crs=Depends(CoordCRSParams),
403 | post_process=Depends(available_algorithms.dependency),
404 | ):
405 | """Create image from part of a dataset."""
406 | with self.reader(self.src_path, **self.reader_params) as src_dst: # type: ignore
407 | if self.nodata is not None and dataset_params.nodata is None:
408 | dataset_params.nodata = self.nodata
409 |
410 | # Adapt options for each reader type
411 | self._update_params(src_dst, layer_params)
412 |
413 | image = src_dst.part(
414 | [minx, miny, maxx, maxy],
415 | dst_crs=dst_crs,
416 | bounds_crs=coord_crs or WGS84_CRS,
417 | **layer_params.as_dict(),
418 | **dataset_params.as_dict(),
419 | **img_params.as_dict(),
420 | )
421 | dst_colormap = getattr(src_dst, "colormap", None)
422 |
423 | if post_process:
424 | image = post_process(image)
425 |
426 | content, media_type = render_image(
427 | image,
428 | output_format=format,
429 | colormap=colormap or dst_colormap,
430 | **render_params.as_dict(),
431 | )
432 |
433 | return Response(content, media_type=media_type)
434 |
435 | feature_params = {
436 | "responses": {
437 | 200: {
438 | "content": img_media_types,
439 | "description": "Return part of a dataset defined by a geojson feature.",
440 | }
441 | },
442 | "response_class": Response,
443 | "description": "Return part of a dataset defined by a geojson feature.",
444 | }
445 |
446 | @self.router.post("/feature", **feature_params, tags=["API"])
447 | @self.router.post("/feature.{format}", **feature_params, tags=["API"])
448 | @self.router.post(
449 | "/feature/{width}x{height}.{format}", **feature_params, tags=["API"]
450 | )
451 | def geojson_part(
452 | geom: Feature,
453 | format: Annotated[Optional[RasterFormat], "Output image type."] = None,
454 | layer_params=Depends(self.layer_dependency),
455 | img_params: PartFeatureParams = Depends(),
456 | dataset_params: DatasetParams = Depends(),
457 | render_params: ImageRenderingParams = Depends(),
458 | colormap: ColorMapParams = Depends(),
459 | post_process=Depends(available_algorithms.dependency),
460 | ):
461 | """Handle /feature requests."""
462 | with self.reader(self.src_path, **self.reader_params) as src_dst: # type: ignore
463 | if self.nodata is not None and dataset_params.nodata is None:
464 | dataset_params.nodata = self.nodata
465 |
466 | # Adapt options for each reader type
467 | self._update_params(src_dst, layer_params)
468 |
469 | image = src_dst.feature(
470 | geom.model_dump(exclude_none=True),
471 | **layer_params.as_dict(),
472 | **dataset_params.as_dict(),
473 | )
474 | dst_colormap = getattr(src_dst, "colormap", None)
475 |
476 | if post_process:
477 | image = post_process(image)
478 |
479 | content, media_type = render_image(
480 | image,
481 | output_format=format,
482 | colormap=colormap or dst_colormap,
483 | **render_params.as_dict(),
484 | )
485 |
486 | return Response(content, media_type=media_type)
487 |
488 | tile_params = {
489 | "responses": {
490 | 200: {
491 | "content": {**img_media_types, **mvt_media_types},
492 | "description": "Return a tile.",
493 | }
494 | },
495 | "response_class": Response,
496 | "description": "Read COG and return a tile",
497 | }
498 |
499 | @self.router.get(
500 | "/tiles/WebMercatorQuad/{z}/{x}/{y}", **tile_params, tags=["API"]
501 | )
502 | @self.router.get(
503 | "/tiles/WebMercatorQuad/{z}/{x}/{y}.{format}", **tile_params, tags=["API"]
504 | )
505 | def tile(
506 | z: Annotated[
507 | int,
508 | Path(
509 | description="Identifier (Z) selecting one of the scales defined in the TileMatrixSet and representing the scaleDenominator the tile.",
510 | ),
511 | ],
512 | x: Annotated[
513 | int,
514 | Path(
515 | description="Column (X) index of the tile on the selected TileMatrix. It cannot exceed the MatrixHeight-1 for the selected TileMatrix.",
516 | ),
517 | ],
518 | y: Annotated[
519 | int,
520 | Path(
521 | description="Row (Y) index of the tile on the selected TileMatrix. It cannot exceed the MatrixWidth-1 for the selected TileMatrix.",
522 | ),
523 | ],
524 | format: Annotated[TileFormat, "Output tile type."] = None,
525 | layer_params=Depends(self.layer_dependency),
526 | dataset_params: DatasetParams = Depends(),
527 | render_params: ImageRenderingParams = Depends(),
528 | tile_params: TileParams = Depends(),
529 | colormap: ColorMapParams = Depends(),
530 | feature_type: Annotated[
531 | Optional[Literal["point", "polygon"]],
532 | Query(title="Feature type (Only for MVT)"),
533 | ] = None,
534 | tilesize: Annotated[
535 | Optional[int],
536 | Query(description="Tile Size."),
537 | ] = None,
538 | post_process=Depends(available_algorithms.dependency),
539 | ):
540 | """Handle /tiles requests."""
541 | default_tilesize = 256
542 |
543 | if format and format in VectorTileFormat:
544 | default_tilesize = 128
545 |
546 | tilesize = tilesize or default_tilesize
547 |
548 | with self.reader(self.src_path, **self.reader_params) as src_dst: # type: ignore
549 | if self.nodata is not None and dataset_params.nodata is None:
550 | dataset_params.nodata = self.nodata
551 |
552 | # Adapt options for each reader type
553 | self._update_params(src_dst, layer_params)
554 |
555 | image = src_dst.tile(
556 | x,
557 | y,
558 | z,
559 | tilesize=tilesize,
560 | **tile_params.as_dict(),
561 | **layer_params.as_dict(),
562 | **dataset_params.as_dict(),
563 | )
564 |
565 | dst_colormap = getattr(src_dst, "colormap", None)
566 |
567 | # Vector Tile
568 | if format and format in VectorTileFormat:
569 | if not pixels_encoder:
570 | raise HTTPException(
571 | status_code=500,
572 | detail="rio-tiler-mvt not found, please do pip install rio-viz['mvt']",
573 | )
574 |
575 | if not feature_type:
576 | raise HTTPException(
577 | status_code=500,
578 | detail="missing feature_type for vector tile.",
579 | )
580 |
581 | content = pixels_encoder(
582 | image.data,
583 | image.mask,
584 | image.band_names,
585 | feature_type=feature_type,
586 | )
587 |
588 | media_type = format.mediatype
589 |
590 | # Raster Tile
591 | else:
592 | if post_process:
593 | image = post_process(image)
594 |
595 | content, media_type = render_image(
596 | image,
597 | output_format=format,
598 | colormap=colormap or dst_colormap,
599 | **render_params.as_dict(),
600 | )
601 |
602 | return Response(content, media_type=media_type)
603 |
604 | @self.router.get(
605 | "/tilejson.json",
606 | response_model=TileJSON,
607 | responses={200: {"description": "Return a tilejson"}},
608 | response_model_exclude_none=True,
609 | tags=["API"],
610 | )
611 | def tilejson(
612 | request: Request,
613 | tile_format: Annotated[
614 | Optional[TileFormat],
615 | "Output tile type.",
616 | ] = None,
617 | layer_params=Depends(self.layer_dependency),
618 | dataset_params: DatasetParams = Depends(),
619 | render_params: ImageRenderingParams = Depends(),
620 | colormap: ColorMapParams = Depends(),
621 | post_process=Depends(available_algorithms.dependency),
622 | feature_type: Annotated[
623 | Optional[Literal["point", "polygon"]],
624 | Query(title="Feature type (Only for MVT)"),
625 | ] = None,
626 | tilesize: Annotated[
627 | Optional[int],
628 | Query(description="Tile Size."),
629 | ] = None,
630 | ):
631 | """Handle /tilejson.json requests."""
632 | kwargs: Dict[str, Any] = {"z": "{z}", "x": "{x}", "y": "{y}"}
633 | if tile_format:
634 | kwargs["format"] = tile_format.value
635 |
636 | tile_url = str(request.url_for("tile", **kwargs))
637 |
638 | qs = [
639 | (key, value)
640 | for (key, value) in request.query_params._list
641 | if key not in ["tile_format"]
642 | ]
643 | if qs:
644 | tile_url += f"?{urllib.parse.urlencode(qs)}"
645 |
646 | with self.reader(self.src_path, **self.reader_params) as src_dst: # type: ignore
647 | bounds = (
648 | self.bounds
649 | if self.bounds is not None
650 | else src_dst.get_geographic_bounds(
651 | src_dst.tms.rasterio_geographic_crs
652 | )
653 | )
654 | minzoom = self.minzoom if self.minzoom is not None else src_dst.minzoom
655 | maxzoom = self.maxzoom if self.maxzoom is not None else src_dst.maxzoom
656 |
657 | return {
658 | "bounds": bounds,
659 | "minzoom": minzoom,
660 | "maxzoom": maxzoom,
661 | "name": "rio-viz",
662 | "tilejson": "2.1.0",
663 | "tiles": [tile_url],
664 | }
665 |
666 | @self.router.get(
667 | "/WMTSCapabilities.xml", response_class=XMLResponse, tags=["API"]
668 | )
669 | def wmts(
670 | request: Request,
671 | tile_format: Annotated[
672 | TileFormat,
673 | Query(description="Output image type. Default is png."),
674 | ] = RasterFormat.png,
675 | layer_params=Depends(self.layer_dependency),
676 | dataset_params: DatasetParams = Depends(),
677 | render_params: ImageRenderingParams = Depends(),
678 | colormap: ColorMapParams = Depends(),
679 | post_process=Depends(available_algorithms.dependency),
680 | feature_type: Annotated[
681 | Optional[Literal["point", "polygon"]],
682 | Query(title="Feature type (Only for MVT)"),
683 | ] = None,
684 | ):
685 | """
686 | This is a hidden gem.
687 |
688 | rio-viz is meant to be use to visualize your dataset in the browser but
689 | using this endpoint, you can also load it in you GIS software.
690 |
691 | """
692 | kwargs = {
693 | "z": "{TileMatrix}",
694 | "x": "{TileCol}",
695 | "y": "{TileRow}",
696 | "format": tile_format.value,
697 | }
698 | tiles_endpoint = str(request.url_for("tile", **kwargs))
699 |
700 | qs = [
701 | (key, value)
702 | for (key, value) in request.query_params._list
703 | if key not in ["tile_format", "REQUEST", "SERVICE"]
704 | ]
705 | if qs:
706 | tiles_endpoint += f"?{urllib.parse.urlencode(qs)}"
707 |
708 | with self.reader(self.src_path, **self.reader_params) as src_dst: # type: ignore
709 | bounds = (
710 | self.bounds
711 | if self.bounds is not None
712 | else src_dst.get_geographic_bounds(
713 | src_dst.tms.rasterio_geographic_crs
714 | )
715 | )
716 | minzoom = self.minzoom if self.minzoom is not None else src_dst.minzoom
717 | maxzoom = self.maxzoom if self.maxzoom is not None else src_dst.maxzoom
718 |
719 | tileMatrix = []
720 | for zoom in range(minzoom, maxzoom + 1): # type: ignore
721 | tm = f"""
722 | {zoom}
723 | {559082264.02872 / 2 ** zoom / 1}
724 | -20037508.34278925 20037508.34278925
725 | 256
726 | 256
727 | {2 ** zoom}
728 | {2 ** zoom}
729 | """
730 | tileMatrix.append(tm)
731 |
732 | return templates.TemplateResponse(
733 | request,
734 | name="wmts.xml",
735 | context={
736 | "tiles_endpoint": tiles_endpoint,
737 | "bounds": bounds,
738 | "tileMatrix": tileMatrix,
739 | "title": "Cloud Optimized GeoTIFF",
740 | "layer_name": "cogeo",
741 | "media_type": tile_format.mediatype,
742 | },
743 | media_type="application/xml",
744 | )
745 |
746 | @self.router.get("/map", response_class=HTMLResponse)
747 | def map_viewer(
748 | request: Request,
749 | tile_format: Annotated[
750 | Optional[RasterFormat],
751 | Query(description="Output raster tile type."),
752 | ] = None,
753 | layer_params=Depends(self.layer_dependency),
754 | dataset_params: DatasetParams = Depends(),
755 | render_params: ImageRenderingParams = Depends(),
756 | colormap: ColorMapParams = Depends(),
757 | post_process=Depends(available_algorithms.dependency),
758 | tilesize: Annotated[
759 | Optional[int],
760 | Query(description="Tile Size."),
761 | ] = None,
762 | ):
763 | """Return a simple map viewer."""
764 | tilejson_url = str(request.url_for("tilejson"))
765 |
766 | if request.query_params:
767 | tilejson_url += f"?{request.query_params}"
768 |
769 | return templates.TemplateResponse(
770 | request,
771 | name="map.html",
772 | context={
773 | "tilejson_endpoint": tilejson_url,
774 | },
775 | media_type="text/html",
776 | )
777 |
778 | @self.router.get(
779 | "/",
780 | responses={200: {"description": "Simple COG viewer."}},
781 | response_class=HTMLResponse,
782 | tags=["Viewer"],
783 | )
784 | @self.router.get(
785 | "/index.html",
786 | responses={200: {"description": "Simple COG viewer."}},
787 | response_class=HTMLResponse,
788 | tags=["Viewer"],
789 | )
790 | def viewer(request: Request):
791 | """Handle /index.html."""
792 | if self.reader_type == "cog":
793 | name = "index.html"
794 | elif self.reader_type == "bands":
795 | name = "bands.html"
796 | elif self.reader_type == "assets":
797 | name = "assets.html"
798 |
799 | return templates.TemplateResponse(
800 | request,
801 | name=name,
802 | context={
803 | "tilejson_endpoint": str(request.url_for("tilejson")),
804 | "stats_endpoint": str(request.url_for("statistics")),
805 | "info_endpoint": str(request.url_for("info_geojson")),
806 | "point_endpoint": str(request.url_for("point")),
807 | "allow_3d": has_mvt,
808 | "geojson": self.geojson,
809 | },
810 | media_type="text/html",
811 | )
812 |
813 | @property
814 | def endpoint(self) -> str:
815 | """Get endpoint url."""
816 | return f"http://{self.host}:{self.port}"
817 |
818 | @property
819 | def template_url(self) -> str:
820 | """Get simple app template url."""
821 | return f"http://{self.host}:{self.port}/index.html"
822 |
823 | @property
824 | def docs_url(self) -> str:
825 | """Get simple app template url."""
826 | return f"http://{self.host}:{self.port}/docs"
827 |
828 | def start(self):
829 | """Start tile server."""
830 | with rasterio.Env(**self.config):
831 | uvicorn.run(app=self.app, host=self.host, port=self.port, log_level="info")
832 |
833 |
834 | @attr.s
835 | class Client(viz):
836 | """Create a Client usable in Jupyter Notebook."""
837 |
838 | server: ServerThread = attr.ib(init=False)
839 |
840 | def __attrs_post_init__(self):
841 | """Update App."""
842 | super().__attrs_post_init__()
843 |
844 | key = f"{self.host}:{self.port}"
845 | if ServerManager.is_server_live(key):
846 | ServerManager.shutdown_server(key)
847 |
848 | self.server = ServerThread(self.app, port=self.port, host=self.host)
849 | ServerManager.add_server(key, self.server)
850 |
851 | def shutdown(self):
852 | """Stop server"""
853 | ServerManager.shutdown_server(f"{self.host}:{self.port}")
854 |
--------------------------------------------------------------------------------
/rio_viz/io/__init__.py:
--------------------------------------------------------------------------------
1 | """rio_viz.io."""
2 |
3 | from rio_viz.io.mosaic import MosaicReader # noqa
4 | from rio_viz.io.reader import MultiFilesAssetsReader, MultiFilesBandsReader # noqa
5 |
--------------------------------------------------------------------------------
/rio_viz/io/mosaic.py:
--------------------------------------------------------------------------------
1 | """rio-viz mosaic reader."""
2 |
3 | from typing import Any, Dict, Type
4 |
5 | import attr
6 | from braceexpand import braceexpand
7 | from morecantile import TileMatrixSet
8 | from rio_tiler.constants import WEB_MERCATOR_TMS, WGS84_CRS
9 | from rio_tiler.io import BaseReader, COGReader
10 | from rio_tiler.models import BandStatistics, ImageData, Info, PointData
11 | from rio_tiler.mosaic import mosaic_point_reader, mosaic_reader
12 |
13 |
14 | @attr.s
15 | class MosaicReader(BaseReader):
16 | """Simple Mosaic reader.
17 |
18 | Args:
19 | input (str): Brace Expandable path (e.g: file{1,2,3}.tif).
20 |
21 | """
22 |
23 | input: str = attr.ib()
24 | tms: TileMatrixSet = attr.ib(default=WEB_MERCATOR_TMS)
25 |
26 | reader: Type[COGReader] = attr.ib(default=COGReader)
27 |
28 | colormap: Dict = attr.ib(init=False)
29 |
30 | datasets: Dict[str, Type[COGReader]] = attr.ib(init=False)
31 |
32 | def __attrs_post_init__(self):
33 | """Fetch Reference band to get the bounds."""
34 | self.datasets = {
35 | src_path: self.reader(src_path, tms=self.tms)
36 | for src_path in braceexpand(self.input)
37 | }
38 |
39 | self.minzoom = min([cog.minzoom for cog in self.datasets.values()])
40 | self.maxzoom = max([cog.maxzoom for cog in self.datasets.values()])
41 |
42 | self.crs = WGS84_CRS
43 | bounds = [cog.get_geographic_bounds(WGS84_CRS) for cog in self.datasets.values()]
44 | minx, miny, maxx, maxy = zip(*bounds)
45 | self.bounds = [min(minx), min(miny), max(maxx), max(maxy)]
46 |
47 | # check for unique dtype
48 | dtypes = {cog.dataset.dtypes[0] for cog in self.datasets.values()}
49 | if len(dtypes) > 1:
50 | raise Exception("Datasets must be of the same data type.")
51 |
52 | # check for same number of band
53 | nbands = {cog.dataset.count for cog in self.datasets.values()}
54 | if len(nbands) > 1:
55 | raise Exception("Datasets must be have the same number of bands.")
56 |
57 | cmaps = [cog.colormap for cog in self.datasets.values() if cog.colormap]
58 | if len(cmaps) > 0:
59 | # !!! We take the first one ¡¡¡
60 | self.colormap = list(cmaps)[0]
61 |
62 | def __exit__(self, exc_type, exc_value, traceback):
63 | """Support using with Context Managers."""
64 | for dataset in self.datasets.values():
65 | dataset.close()
66 |
67 | def tile(
68 | self,
69 | tile_x: int,
70 | tile_y: int,
71 | tile_z: int,
72 | reverse: bool = False,
73 | **kwargs: Any,
74 | ) -> ImageData:
75 | """Get Tile."""
76 | mosaic_assets = (
77 | list(reversed(list(self.datasets))) if reverse else list(self.datasets)
78 | )
79 |
80 | def _reader(
81 | asset: str, tile_x: int, tile_y: int, tile_z: int, **kwargs: Any
82 | ) -> ImageData:
83 | return self.datasets[asset].tile(tile_x, tile_y, tile_z, **kwargs)
84 |
85 | return mosaic_reader(
86 | mosaic_assets, _reader, tile_x, tile_y, tile_z, threads=0, **kwargs
87 | )[0]
88 |
89 | def point(
90 | self,
91 | lon: float,
92 | lat: float,
93 | reverse: bool = False,
94 | **kwargs: Any,
95 | ) -> PointData:
96 | """Get Point value."""
97 | mosaic_assets = (
98 | list(reversed(list(self.datasets))) if reverse else list(self.datasets)
99 | )
100 |
101 | def _reader(asset: str, lon: float, lat: float, **kwargs) -> PointData:
102 | return self.datasets[asset].point(lon, lat, **kwargs)
103 |
104 | return mosaic_point_reader(mosaic_assets, _reader, lon, lat, threads=0, **kwargs)[
105 | 0
106 | ]
107 |
108 | def info(self) -> Info:
109 | """info."""
110 | # !!! We return info from the first dataset
111 | # Most of the info should be similar in other files ¡¡¡
112 | item = list(self.datasets)[0]
113 | info_metadata = (
114 | self.datasets[item]
115 | .info()
116 | .model_dump(
117 | exclude={
118 | "bounds",
119 | "minzoom",
120 | "maxzoom",
121 | "width",
122 | "height",
123 | "overviews",
124 | },
125 | )
126 | )
127 | info_metadata["bounds"] = self.bounds
128 | info_metadata["minzoom"] = self.minzoom
129 | info_metadata["maxzoom"] = self.maxzoom
130 | return Info(**info_metadata)
131 |
132 | def statistics(self, **kwargs: Any) -> Dict[str, BandStatistics]:
133 | """Return Dataset's statistics."""
134 | # FOR NOW WE ONLY RETURN VALUE FROM THE FIRST FILE
135 | item = list(self.datasets)[0]
136 | return self.datasets[item].statistics(**kwargs)
137 |
138 | ############################################################################
139 | # Not Implemented methods
140 | # BaseReader required those method to be implemented
141 | def preview(self, *args, **kwargs):
142 | """Placeholder for BaseReader.preview."""
143 | raise NotImplementedError
144 |
145 | def part(self, *args, **kwargs):
146 | """Placeholder for BaseReader.part."""
147 | raise NotImplementedError
148 |
149 | def feature(self, *args, **kwargs):
150 | """Placeholder for BaseReader.feature."""
151 | raise NotImplementedError
152 |
--------------------------------------------------------------------------------
/rio_viz/io/reader.py:
--------------------------------------------------------------------------------
1 | """rio-viz multifile reader."""
2 |
3 | from typing import List, Type
4 |
5 | import attr
6 | from braceexpand import braceexpand
7 | from rio_tiler import io
8 | from rio_tiler.errors import InvalidBandName
9 | from rio_tiler.types import AssetInfo
10 |
11 |
12 | @attr.s
13 | class MultiFilesBandsReader(io.MultiBandReader):
14 | """Multiple Files as Bands."""
15 |
16 | reader: Type[io.BaseReader] = attr.ib(default=io.Reader, init=False)
17 |
18 | _files: List[str] = attr.ib(init=False)
19 |
20 | def __attrs_post_init__(self):
21 | """Fetch Reference band to get the bounds."""
22 | self._files = list(braceexpand(self.input))
23 | self.bands = [f"b{ix + 1}" for ix in range(len(self._files))]
24 |
25 | with self.reader(self._files[0], tms=self.tms, **self.reader_options) as cog:
26 | self.bounds = cog.bounds
27 | self.crs = cog.crs
28 | self.minzoom = cog.minzoom
29 | self.maxzoom = cog.maxzoom
30 |
31 | def _get_band_url(self, band: str) -> str:
32 | """Validate band's name and return band's url."""
33 | if band not in self.bands:
34 | raise InvalidBandName(f"{band} is not valid")
35 |
36 | index = self.bands.index(band)
37 | return self._files[index]
38 |
39 |
40 | @attr.s
41 | class MultiFilesAssetsReader(io.MultiBaseReader):
42 | """Multiple Files as Assets."""
43 |
44 | reader: Type[io.BaseReader] = attr.ib(default=io.Reader, init=False)
45 |
46 | _files: List[str] = attr.ib(init=False)
47 |
48 | def __attrs_post_init__(self):
49 | """Fetch Reference band to get the bounds."""
50 | self._files = list(braceexpand(self.input))
51 | self.assets = [f"asset{ix + 1}" for ix in range(len(self._files))]
52 |
53 | with self.reader(self._files[0], tms=self.tms, **self.reader_options) as cog:
54 | self.bounds = cog.bounds
55 | self.crs = cog.crs
56 | self.minzoom = cog.minzoom
57 | self.maxzoom = cog.maxzoom
58 |
59 | def _get_asset_info(self, asset: str) -> AssetInfo:
60 | """Validate band's name and return band's url."""
61 | if asset not in self.assets:
62 | raise InvalidBandName(f"{asset} is not valid")
63 |
64 | index = self.assets.index(asset)
65 | return AssetInfo(url=self._files[index])
66 |
--------------------------------------------------------------------------------
/rio_viz/resources/__init__.py:
--------------------------------------------------------------------------------
1 | """titiler.resources."""
2 |
--------------------------------------------------------------------------------
/rio_viz/resources/enums.py:
--------------------------------------------------------------------------------
1 | """rio-viz Enums."""
2 |
3 | from enum import Enum
4 | from types import DynamicClassAttribute
5 |
6 | from rio_tiler.profiles import img_profiles
7 |
8 |
9 | class MediaType(str, Enum):
10 | """Responses Media types formerly known as MIME types."""
11 |
12 | tif = "image/tiff; application=geotiff"
13 | jp2 = "image/jp2"
14 | png = "image/png"
15 | pngraw = "image/png"
16 | jpeg = "image/jpeg"
17 | jpg = "image/jpg"
18 | webp = "image/webp"
19 | npy = "application/x-binary"
20 | xml = "application/xml"
21 | json = "application/json"
22 | geojson = "application/geo+json"
23 | html = "text/html"
24 | text = "text/plain"
25 | pbf = "application/x-protobuf"
26 | mvt = "application/x-protobuf"
27 |
28 |
29 | class ImageDriver(str, Enum):
30 | """Supported output GDAL drivers."""
31 |
32 | jpeg = "JPEG"
33 | jpg = "JPEG"
34 | png = "PNG"
35 | pngraw = "PNG"
36 | tif = "GTiff"
37 | webp = "WEBP"
38 | jp2 = "JP2OpenJPEG"
39 | npy = "NPY"
40 |
41 |
42 | class DataFormat(str, Enum):
43 | """Data Format Base Class."""
44 |
45 | @DynamicClassAttribute
46 | def profile(self):
47 | """Return rio-tiler image default profile."""
48 | return img_profiles.get(self._name_, {})
49 |
50 | @DynamicClassAttribute
51 | def driver(self):
52 | """Return rio-tiler image default profile."""
53 | try:
54 | return ImageDriver[self._name_].value
55 | except KeyError:
56 | return ""
57 |
58 | @DynamicClassAttribute
59 | def mediatype(self):
60 | """Return image mimetype."""
61 | return MediaType[self._name_].value
62 |
63 |
64 | class RasterFormat(DataFormat):
65 | """Available Output Raster format."""
66 |
67 | png = "png"
68 | npy = "npy"
69 | tif = "tif"
70 | jpeg = "jpeg"
71 | jpg = "jpg"
72 | jp2 = "jp2"
73 | webp = "webp"
74 | pngraw = "pngraw"
75 |
76 |
77 | class VectorTileFormat(DataFormat):
78 | """Available Output Vector Tile format."""
79 |
80 | pbf = "pbf"
81 | mvt = "mvt"
82 |
--------------------------------------------------------------------------------
/rio_viz/scripts/__init__.py:
--------------------------------------------------------------------------------
1 | """rio_viz cli."""
2 |
--------------------------------------------------------------------------------
/rio_viz/scripts/cli.py:
--------------------------------------------------------------------------------
1 | """rio_viz.cli."""
2 |
3 | import importlib
4 | import json
5 | import os
6 | import tempfile
7 | import warnings
8 | from contextlib import ExitStack, contextmanager
9 |
10 | import click
11 | import numpy
12 | from rasterio.rio import options
13 | from rio_cogeo.cogeo import cog_translate, cog_validate
14 | from rio_cogeo.profiles import cog_profiles
15 | from rio_tiler.io import BaseReader, COGReader, MultiBandReader, MultiBaseReader
16 |
17 | from rio_viz import app
18 |
19 |
20 | def options_to_dict(ctx, param, value):
21 | """
22 | click callback to validate `--opt KEY1=VAL1 --opt KEY2=VAL2` and collect
23 | in a dictionary like the one below, which is what the CLI function receives.
24 | If no value or `None` is received then an empty dictionary is returned.
25 |
26 | {
27 | 'KEY1': 'VAL1',
28 | 'KEY2': 'VAL2'
29 | }
30 |
31 | Note: `==VAL` breaks this as `str.split('=', 1)` is used.
32 | """
33 |
34 | if not value:
35 | return {}
36 | else:
37 | out = {}
38 | for pair in value:
39 | if "=" not in pair:
40 | raise click.BadParameter(f"Invalid syntax for KEY=VAL arg: {pair}")
41 | else:
42 | k, v = pair.split("=", 1)
43 | out[k] = v
44 |
45 | return out
46 |
47 |
48 | @contextmanager
49 | def TemporaryRasterFile(suffix=".tif"):
50 | """Create temporary file."""
51 | fileobj = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
52 | fileobj.close()
53 | try:
54 | yield fileobj
55 | finally:
56 | os.remove(fileobj.name)
57 |
58 |
59 | class NodataParamType(click.ParamType):
60 | """Nodata index type."""
61 |
62 | name = "nodata"
63 |
64 | def convert(self, value, param, ctx):
65 | """Validate and parse band index."""
66 | try:
67 | if value.lower() == "nan":
68 | return numpy.nan
69 | elif value.lower() in ["nil", "none", "nada"]:
70 | return None
71 | else:
72 | return float(value)
73 | except (TypeError, ValueError) as e:
74 | raise click.ClickException(
75 | "{} is not a valid nodata value.".format(value)
76 | ) from e
77 |
78 |
79 | @click.command()
80 | @click.argument("src_path", type=str, nargs=1, required=True)
81 | @click.option(
82 | "--nodata",
83 | type=NodataParamType(),
84 | metavar="NUMBER|nan",
85 | help="Set nodata masking values for input dataset.",
86 | )
87 | @click.option(
88 | "--minzoom",
89 | type=int,
90 | help="Overwrite minzoom",
91 | )
92 | @click.option(
93 | "--maxzoom",
94 | type=int,
95 | help="Overwrite maxzoom",
96 | )
97 | @click.option("--port", type=int, default=8080, help="Webserver port (default: 8080)")
98 | @click.option(
99 | "--host",
100 | type=str,
101 | default="127.0.0.1",
102 | help="Webserver host url (default: 127.0.0.1)",
103 | )
104 | @click.option("--no-check", is_flag=True, help="Ignore COG validation")
105 | @click.option(
106 | "--reader",
107 | type=str,
108 | help="rio-tiler Reader (BaseReader). Default is `rio_tiler.io.COGReader`",
109 | )
110 | @click.option(
111 | "--layers",
112 | type=str,
113 | help="limit to specific layers (only used for MultiBand and MultiBase Readers) (e.g --layers b1 --layers b2).",
114 | multiple=True,
115 | )
116 | @click.option(
117 | "--server-only",
118 | is_flag=True,
119 | default=False,
120 | help="Launch API without opening the rio-viz web-page.",
121 | )
122 | @click.option(
123 | "--config",
124 | "config",
125 | metavar="NAME=VALUE",
126 | multiple=True,
127 | callback=options._cb_key_val,
128 | help="GDAL configuration options.",
129 | )
130 | @click.option(
131 | "--reader-params",
132 | "-p",
133 | "reader_params",
134 | metavar="NAME=VALUE",
135 | multiple=True,
136 | callback=options_to_dict,
137 | help="Reader Options.",
138 | )
139 | @click.option(
140 | "--geojson",
141 | type=click.File(mode="r"),
142 | help="GeoJSON Feature or FeatureCollection path to display on viewer.",
143 | )
144 | def viz(
145 | src_path,
146 | nodata,
147 | minzoom,
148 | maxzoom,
149 | port,
150 | host,
151 | no_check,
152 | reader,
153 | layers,
154 | server_only,
155 | config,
156 | reader_params,
157 | geojson,
158 | ):
159 | """Rasterio Viz cli."""
160 | if reader:
161 | module, classname = reader.rsplit(".", 1)
162 | reader = getattr(importlib.import_module(module), classname) # noqa
163 | if not issubclass(reader, (BaseReader, MultiBandReader, MultiBaseReader)):
164 | warnings.warn(f"Invalid reader type: {type(reader)}")
165 |
166 | dataset_reader = reader or COGReader
167 |
168 | # Check if cog
169 | with ExitStack() as ctx:
170 | if (
171 | src_path.lower().endswith(".tif")
172 | and not reader
173 | and not no_check
174 | and not cog_validate(src_path)[0]
175 | ):
176 | # create tmp COG
177 | click.echo("create temporary COG")
178 | tmp_path = ctx.enter_context(TemporaryRasterFile())
179 |
180 | output_profile = cog_profiles.get("deflate")
181 | output_profile.update({"blockxsize": "256", "blockysize": "256"})
182 | config = {"GDAL_TIFF_INTERNAL_MASK": True, "GDAL_TIFF_OVR_BLOCKSIZE": "128"}
183 | cog_translate(src_path, tmp_path.name, output_profile, config=config)
184 | src_path = tmp_path.name
185 |
186 | application = app.viz(
187 | src_path=src_path,
188 | reader=dataset_reader,
189 | reader_params=reader_params,
190 | port=port,
191 | host=host,
192 | config=config,
193 | minzoom=minzoom,
194 | maxzoom=maxzoom,
195 | nodata=nodata,
196 | layers=layers,
197 | geojson=json.load(geojson) if geojson else None,
198 | )
199 | if not server_only:
200 | click.echo(f"Viewer started at {application.template_url}", err=True)
201 | click.launch(application.template_url)
202 |
203 | application.start()
204 |
--------------------------------------------------------------------------------
/rio_viz/templates/assets.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | rio-viz for Asset Reader (STAC)
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
186 |
187 |
188 |
298 |
299 |
309 |
310 |
1085 |
1086 |
1087 |
--------------------------------------------------------------------------------
/rio_viz/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Rio Viz
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
182 |
183 |
184 |
307 |
308 |
318 |
319 |
1204 |
1205 |
1206 |
--------------------------------------------------------------------------------
/rio_viz/templates/map.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Rio Viz
6 |
7 |
8 |
9 |
10 |
32 |
33 |
34 |
35 |
38 |
39 |
196 |
197 |
198 |
--------------------------------------------------------------------------------
/rio_viz/templates/wmts.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | "{{ title }}"
4 | OGC WMTS
5 | 1.0.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | RESTful
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | RESTful
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | {{ title }}
38 | {{ layer_name }}
39 | {{ title }}
40 |
41 | {{ bounds[0] }} {{ bounds[1] }}
42 | {{ bounds[2] }} {{ bounds[3] }}
43 |
44 |
47 | {{ media_type }}
48 |
49 | GoogleMapsCompatible
50 |
51 |
52 |
53 |
54 | GoogleMapsCompatible
55 | urn:ogc:def:crs:EPSG::3857
56 | {% for item in tileMatrix %}
57 | {{ item | safe }}
58 | {% endfor %}
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/tests/fixtures/cog.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/rio-viz/c8676343ae2f84f17322372da481f5194ad3f016/tests/fixtures/cog.tif
--------------------------------------------------------------------------------
/tests/fixtures/cogb1.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/rio-viz/c8676343ae2f84f17322372da481f5194ad3f016/tests/fixtures/cogb1.tif
--------------------------------------------------------------------------------
/tests/fixtures/cogb2.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/rio-viz/c8676343ae2f84f17322372da481f5194ad3f016/tests/fixtures/cogb2.tif
--------------------------------------------------------------------------------
/tests/fixtures/cogb3.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/rio-viz/c8676343ae2f84f17322372da481f5194ad3f016/tests/fixtures/cogb3.tif
--------------------------------------------------------------------------------
/tests/fixtures/mosaic_cog1.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/rio-viz/c8676343ae2f84f17322372da481f5194ad3f016/tests/fixtures/mosaic_cog1.tif
--------------------------------------------------------------------------------
/tests/fixtures/mosaic_cog2.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/rio-viz/c8676343ae2f84f17322372da481f5194ad3f016/tests/fixtures/mosaic_cog2.tif
--------------------------------------------------------------------------------
/tests/fixtures/noncog.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/developmentseed/rio-viz/c8676343ae2f84f17322372da481f5194ad3f016/tests/fixtures/noncog.tif
--------------------------------------------------------------------------------
/tests/test_app.py:
--------------------------------------------------------------------------------
1 | """tests rio_viz.server."""
2 |
3 | import json
4 | import os
5 |
6 | import pytest
7 | from rio_tiler.io import COGReader
8 | from starlette.testclient import TestClient
9 |
10 | from rio_viz.app import viz
11 | from rio_viz.io.mosaic import MosaicReader
12 | from rio_viz.io.reader import MultiFilesAssetsReader, MultiFilesBandsReader
13 |
14 | cog_path = os.path.join(os.path.dirname(__file__), "fixtures", "cog.tif")
15 | cogb1b2b3_path = os.path.join(os.path.dirname(__file__), "fixtures", "cogb{1,2,3}.tif")
16 | cog_mosaic_path = os.path.join(
17 | os.path.dirname(__file__), "fixtures", "mosaic_cog{1,2}.tif"
18 | )
19 |
20 |
21 | def test_viz():
22 | """Should work as expected (create TileServer object)."""
23 | src_path = cog_path
24 | dataset_reader = COGReader
25 |
26 | app = viz(src_path, reader=dataset_reader)
27 |
28 | assert app.port == 8080
29 | assert app.endpoint == "http://127.0.0.1:8080"
30 | assert app.template_url == "http://127.0.0.1:8080/index.html"
31 |
32 | client = TestClient(app.app)
33 | response = client.get("/")
34 | assert response.status_code == 200
35 | assert response.headers["cache-control"] == "no-cache"
36 |
37 | response = client.get("/index.html")
38 | assert response.status_code == 200
39 | assert response.headers["cache-control"] == "no-cache"
40 |
41 | response = client.get("/tiles/WebMercatorQuad/7/64/43.png?rescale=1,10")
42 | assert response.status_code == 200
43 | assert response.headers["content-type"] == "image/png"
44 | assert response.headers["cache-control"] == "no-cache"
45 |
46 | response = client.get(
47 | "/tiles/WebMercatorQuad/7/64/43.png?rescale=1,10&bidx=1&color_formula=Gamma R 3"
48 | )
49 | assert response.status_code == 200
50 | assert response.headers["content-type"] == "image/png"
51 |
52 | response = client.get(
53 | "/tiles/WebMercatorQuad/7/64/43.png?rescale=1,10&bidx=1&bidx=1&bidx=1"
54 | )
55 | assert response.status_code == 200
56 | assert response.headers["content-type"] == "image/png"
57 |
58 | response = client.get(
59 | "/tiles/WebMercatorQuad/7/64/43.png?rescale=1,10&colormap_name=cfastie"
60 | )
61 | assert response.status_code == 200
62 | assert response.headers["content-type"] == "image/png"
63 |
64 | response = client.get(
65 | "/tiles/WebMercatorQuad/7/64/43?rescale=1,10&colormap_name=cfastie"
66 | )
67 | assert response.status_code == 200
68 | assert response.headers["content-type"] == "image/png"
69 |
70 | response = client.get("/tiles/WebMercatorQuad/18/8624/119094.png")
71 | assert response.status_code == 404
72 |
73 | response = client.get("/tiles/WebMercatorQuad/18/8624/119094.pbf")
74 | assert response.status_code == 404
75 |
76 | response = client.get("/tiles/WebMercatorQuad/7/64/43.pbf")
77 | assert response.status_code == 500
78 | assert not response.headers.get("cache-control")
79 |
80 | response = client.get("/tiles/WebMercatorQuad/7/64/43.pbf?feature_type=polygon")
81 | assert response.status_code == 200
82 | assert response.headers["content-type"] == "application/x-protobuf"
83 |
84 | response = client.get("/tiles/WebMercatorQuad/7/64/43.pbf?feature_type=point")
85 | assert response.status_code == 200
86 | assert response.headers["content-type"] == "application/x-protobuf"
87 |
88 | response = client.get("/preview?rescale=1,10&colormap_name=cfastie")
89 | assert response.status_code == 200
90 | assert response.headers["content-type"] == "image/jpeg"
91 | assert response.headers["cache-control"] == "no-cache"
92 |
93 | response = client.get("/preview.png?rescale=1,10&colormap_name=cfastie")
94 | assert response.status_code == 200
95 | assert response.headers["content-type"] == "image/png"
96 |
97 | response = client.get(
98 | "/bbox/-2.00,48.5,-1,49.5.png?rescale=1,10&colormap_name=cfastie"
99 | )
100 | assert response.status_code == 200
101 | assert response.headers["content-type"] == "image/png"
102 |
103 | response = client.get(
104 | "/bbox/-2.00,48.5,-1,49.5/100x100.jpeg?&rescale=1,10&colormap_name=cfastie"
105 | )
106 | assert response.status_code == 200
107 | assert response.headers["content-type"] == "image/jpeg"
108 |
109 | response = client.get("/info")
110 | assert response.status_code == 200
111 | assert response.headers["content-type"] == "application/json"
112 |
113 | response = client.get("/statistics")
114 | assert response.status_code == 200
115 | assert response.headers["content-type"] == "application/json"
116 |
117 | response = client.get("/tilejson.json?tile_format=png")
118 | assert response.status_code == 200
119 | assert response.headers["content-type"] == "application/json"
120 | r = response.json()
121 | assert r["bounds"]
122 | assert r["center"]
123 | assert r["minzoom"] == 7
124 | assert r["maxzoom"] == 9
125 | assert r["tiles"][0].endswith("png")
126 |
127 | response = client.get("/tilejson.json?tile_format=pbf")
128 | assert response.status_code == 200
129 | assert response.headers["content-type"] == "application/json"
130 | r = response.json()
131 | assert r["tiles"][0].endswith("pbf")
132 |
133 | response = client.get("/tilejson.json?tile_format=pbf&feature_type=polygon")
134 | assert response.status_code == 200
135 | assert response.headers["content-type"] == "application/json"
136 | r = response.json()
137 | assert r["tiles"][0].endswith("pbf?feature_type=polygon")
138 |
139 | response = client.get("/point?coordinates=-2,48")
140 | assert response.status_code == 200
141 | assert response.headers["content-type"] == "application/json"
142 | assert response.json() == {
143 | "band_names": ["b1"],
144 | "coordinates": [-2.0, 48.0],
145 | "values": [110],
146 | }
147 |
148 | feat = json.dumps(
149 | {
150 | "type": "Feature",
151 | "properties": {},
152 | "geometry": {
153 | "type": "Polygon",
154 | "coordinates": [
155 | [
156 | [-1.8511962890624998, 49.296471602658066],
157 | [-2.5213623046875, 48.56388521347092],
158 | [-2.1258544921875, 48.213692646648035],
159 | [-1.4556884765625, 48.356249029540734],
160 | [-1.1590576171875, 48.469279317167164],
161 | [-0.8184814453125, 49.46455408928758],
162 | [-1.4666748046875, 49.55728898983402],
163 | [-1.64794921875, 49.50380954152213],
164 | [-1.8511962890624998, 49.296471602658066],
165 | ]
166 | ],
167 | },
168 | }
169 | )
170 |
171 | response = client.post("/feature", data=feat)
172 | assert response.status_code == 200
173 | assert response.headers["content-type"] == "image/png"
174 |
175 | response = client.post("/feature.jpeg", data=feat)
176 | assert response.status_code == 200
177 | assert response.headers["content-type"] == "image/jpeg"
178 |
179 | response = client.post("/feature/100x100.jpeg", data=feat)
180 | assert response.status_code == 200
181 | assert response.headers["content-type"] == "image/jpeg"
182 |
183 | response = client.post(
184 | "/feature.jpeg",
185 | params={"bidx": 1, "rescale": "1,10", "colormap_name": "cfastie"},
186 | data=feat,
187 | )
188 | assert response.status_code == 200
189 | assert response.headers["content-type"] == "image/jpeg"
190 |
191 |
192 | def test_viz_custom():
193 | """Should work as expected (create TileServer object)."""
194 | src_path = cog_path
195 | app = viz(src_path, reader=COGReader, host="0.0.0.0", port=5050)
196 | assert app.port == 5050
197 | assert app.endpoint == "http://0.0.0.0:5050"
198 |
199 |
200 | def test_viz_multibands():
201 | """Should work as expected (create TileServer object)."""
202 | dataset_reader = MultiFilesBandsReader
203 |
204 | # Use default bands from the reader
205 | app = viz(cogb1b2b3_path, reader=dataset_reader)
206 | assert app.port == 8080
207 | assert app.endpoint == "http://127.0.0.1:8080"
208 | client = TestClient(app.app)
209 |
210 | response = client.get("/info")
211 | assert response.status_code == 200
212 | assert response.headers["content-type"] == "application/json"
213 | assert response.json()["band_descriptions"] == [
214 | ["b1", ""],
215 | ["b2", ""],
216 | ["b3", ""],
217 | ]
218 |
219 | response = client.get("/info?bands=b1&bands=b2")
220 | assert response.status_code == 200
221 | assert response.headers["content-type"] == "application/json"
222 | assert response.json()["band_descriptions"] == [
223 | ["b1", ""],
224 | ["b2", ""],
225 | ]
226 |
227 | response = client.get("/statistics")
228 | assert response.status_code == 200
229 | assert response.headers["content-type"] == "application/json"
230 | assert ["b1", "b2", "b3"] == list(response.json())
231 |
232 | response = client.get("/statistics?bands=b1&bands=b2")
233 | assert response.status_code == 200
234 | assert response.headers["content-type"] == "application/json"
235 | assert ["b1", "b2"] == list(response.json())
236 |
237 | response = client.get("/point?coordinates=-2.0,48.0")
238 | assert response.status_code == 200
239 | assert response.headers["content-type"] == "application/json"
240 | assert len(response.json()["values"]) == 3
241 |
242 | response = client.get("/point?coordinates=-2.0,48.0&bands=b1&bands=b2")
243 | assert response.status_code == 200
244 | assert response.headers["content-type"] == "application/json"
245 | assert len(response.json()["values"]) == 2
246 |
247 | # Set default bands (other bands might still be available within the reader)
248 | app = viz(cogb1b2b3_path, reader=dataset_reader, layers=["b1"])
249 | assert app.port == 8080
250 | assert app.endpoint == "http://127.0.0.1:8080"
251 | client = TestClient(app.app)
252 |
253 | response = client.get("/info")
254 | assert response.status_code == 200
255 | assert response.headers["content-type"] == "application/json"
256 | assert response.json()["band_descriptions"] == [
257 | ["b1", ""],
258 | ]
259 |
260 | response = client.get("/info?bands=b1&bands=b2")
261 | assert response.status_code == 200
262 | assert response.headers["content-type"] == "application/json"
263 | assert response.json()["band_descriptions"] == [
264 | ["b1", ""],
265 | ["b2", ""],
266 | ]
267 |
268 | response = client.get("/statistics")
269 | assert response.status_code == 200
270 | assert response.headers["content-type"] == "application/json"
271 | assert ["b1"] == list(response.json())
272 |
273 | response = client.get("/statistics?bands=b1&bands=b2")
274 | assert response.status_code == 200
275 | assert response.headers["content-type"] == "application/json"
276 | assert ["b1", "b2"] == list(response.json())
277 |
278 | response = client.get("/point?coordinates=-2.0,48.0")
279 | assert response.status_code == 200
280 | assert response.headers["content-type"] == "application/json"
281 | assert len(response.json()["values"]) == 1
282 |
283 | response = client.get("/point?coordinates=-2.0,48.0&bands=b1&bands=b2")
284 | assert response.status_code == 200
285 | assert response.headers["content-type"] == "application/json"
286 | assert len(response.json()["values"]) == 2
287 |
288 |
289 | def test_viz_multiassets():
290 | """Should work as expected (create TileServer object)."""
291 | dataset_reader = MultiFilesAssetsReader
292 | # Use default bands from the reader
293 | app = viz(cogb1b2b3_path, reader=dataset_reader)
294 | assert app.port == 8080
295 | assert app.endpoint == "http://127.0.0.1:8080"
296 | client = TestClient(app.app)
297 |
298 | response = client.get("/info")
299 | assert response.status_code == 200
300 | assert response.headers["content-type"] == "application/json"
301 | assert ["asset1", "asset2", "asset3"] == list(response.json())
302 | assert response.json()["asset1"]["band_descriptions"]
303 |
304 | response = client.get("/info?assets=asset1&assets=asset2")
305 | assert response.status_code == 200
306 | assert response.headers["content-type"] == "application/json"
307 | assert ["asset1", "asset2"] == list(response.json())
308 |
309 | response = client.get("/statistics")
310 | assert response.status_code == 200
311 | assert response.headers["content-type"] == "application/json"
312 | assert ["asset1", "asset2", "asset3"] == list(response.json())
313 |
314 | response = client.get("/statistics?assets=asset1&assets=asset2")
315 | assert response.status_code == 200
316 | assert response.headers["content-type"] == "application/json"
317 | assert ["asset1", "asset2"] == list(response.json())
318 |
319 | response = client.get("/point?coordinates=-2.0,48.0")
320 | assert response.status_code == 200
321 | assert response.headers["content-type"] == "application/json"
322 | assert len(response.json()["values"]) == 3
323 | assert response.json()["band_names"] == ["asset1_b1", "asset2_b1", "asset3_b1"]
324 |
325 | response = client.get("/point?coordinates=-2.0,48.0&assets=asset1&assets=asset2")
326 | assert response.status_code == 200
327 | assert response.headers["content-type"] == "application/json"
328 | assert len(response.json()["values"]) == 2
329 | assert response.json()["band_names"] == ["asset1_b1", "asset2_b1"]
330 |
331 | # Set default bands (other bands might still be available within the reader)
332 | app = viz(cogb1b2b3_path, reader=dataset_reader, layers=["asset1"])
333 | assert app.port == 8080
334 | assert app.endpoint == "http://127.0.0.1:8080"
335 | client = TestClient(app.app)
336 |
337 | response = client.get("/info")
338 | assert response.status_code == 200
339 | assert response.headers["content-type"] == "application/json"
340 | assert ["asset1"] == list(response.json())
341 |
342 | response = client.get("/info?assets=asset1&assets=asset2")
343 | assert response.status_code == 200
344 | assert response.headers["content-type"] == "application/json"
345 | assert ["asset1", "asset2"] == list(response.json())
346 |
347 | response = client.get("/statistics")
348 | assert response.status_code == 200
349 | assert response.headers["content-type"] == "application/json"
350 | assert ["asset1"] == list(response.json())
351 |
352 | response = client.get("/statistics?assets=asset1&assets=asset2")
353 | assert response.status_code == 200
354 | assert response.headers["content-type"] == "application/json"
355 | assert ["asset1", "asset2"] == list(response.json())
356 |
357 | response = client.get("/point?coordinates=-2.0,48.0")
358 | assert response.status_code == 200
359 | assert response.headers["content-type"] == "application/json"
360 | assert len(response.json()["values"]) == 1
361 |
362 | response = client.get("/point?coordinates=-2.0,48.0&assets=asset1&assets=asset2")
363 | assert response.status_code == 200
364 | assert response.headers["content-type"] == "application/json"
365 | assert len(response.json()["values"]) == 2
366 |
367 |
368 | def test_viz_mosaic():
369 | """Should work as expected (create TileServer object)."""
370 | src_path = cog_mosaic_path
371 | dataset_reader = MosaicReader
372 |
373 | app = viz(src_path, reader=dataset_reader)
374 |
375 | assert app.port == 8080
376 | assert app.endpoint == "http://127.0.0.1:8080"
377 | assert app.template_url == "http://127.0.0.1:8080/index.html"
378 |
379 | client = TestClient(app.app)
380 | response = client.get("/")
381 | assert response.status_code == 200
382 | assert response.headers["cache-control"] == "no-cache"
383 |
384 | response = client.get("/index.html")
385 | assert response.status_code == 200
386 | assert response.headers["cache-control"] == "no-cache"
387 |
388 | response = client.get("/info")
389 | assert response.status_code == 200
390 | assert response.headers["content-type"] == "application/json"
391 |
392 | response = client.get("/statistics")
393 | assert response.status_code == 200
394 | assert response.headers["content-type"] == "application/json"
395 |
396 | response = client.get("/tiles/WebMercatorQuad/8/75/91?rescale=1,10")
397 | assert response.status_code == 200
398 | assert response.headers["content-type"] == "image/png"
399 | assert response.headers["cache-control"] == "no-cache"
400 |
401 | with pytest.raises(NotImplementedError):
402 | client.get("/preview")
403 |
404 | with pytest.raises(NotImplementedError):
405 | client.get("/bbox/-2.00,48.5,-1,49.5.png")
406 |
407 | # Point is Outside COGs bounds
408 | response = client.get("/point?coordinates=-2,48")
409 | assert response.status_code == 500
410 | assert "Method returned an empty array" in response.text # InvalidPointDataError
411 |
412 | response = client.get("/point?coordinates=-72.63567185076337,46.10493842126715")
413 | assert response.status_code == 200
414 | assert response.headers["content-type"] == "application/json"
415 | assert response.json() == {
416 | "band_names": ["b1", "b2", "b3"],
417 | "coordinates": [-72.63567185076337, 46.10493842126715],
418 | "values": [12453, 11437, 11360],
419 | }
420 |
--------------------------------------------------------------------------------
/tests/test_cli.py:
--------------------------------------------------------------------------------
1 | """tests rio_viz.server."""
2 |
3 | import os
4 | from unittest.mock import patch
5 |
6 | from click.testing import CliRunner
7 |
8 | from rio_viz.scripts.cli import viz
9 |
10 | cog_path = os.path.join(os.path.dirname(__file__), "fixtures", "cog.tif")
11 | noncog_path = os.path.join(os.path.dirname(__file__), "fixtures", "noncog.tif")
12 |
13 |
14 | @patch("rio_viz.app.viz")
15 | @patch("click.launch")
16 | def test_viz_valid(launch, app):
17 | """Should work as expected."""
18 | app.return_value.get_template_url.return_value = "http://127.0.0.1:8080/index.html"
19 | app.return_value.start.return_value = True
20 |
21 | launch.return_value = True
22 |
23 | runner = CliRunner()
24 | result = runner.invoke(viz, [cog_path])
25 | app.assert_called_once()
26 | assert not result.exception
27 | assert result.exit_code == 0
28 |
29 |
30 | @patch("rio_viz.app.viz")
31 | @patch("click.launch")
32 | def test_viz_invalidCog(launch, app):
33 | """Should work as expected."""
34 | app.return_value.start.return_value = True
35 | launch.return_value = True
36 |
37 | runner = CliRunner()
38 | result = runner.invoke(viz, [noncog_path])
39 | assert app.call_args[0] is not noncog_path
40 | app.assert_called_once()
41 | assert not result.exception
42 | assert result.exit_code == 0
43 |
--------------------------------------------------------------------------------
/tests/test_enums.py:
--------------------------------------------------------------------------------
1 | """test rio-viz enums."""
2 |
3 | import pytest
4 |
5 | from rio_viz.resources.enums import RasterFormat, VectorTileFormat
6 |
7 |
8 | @pytest.mark.parametrize(
9 | "value,driver,mimetype",
10 | [
11 | ("png", "PNG", "image/png"),
12 | ("npy", "NPY", "application/x-binary"),
13 | ("tif", "GTiff", "image/tiff; application=geotiff"),
14 | ("jpeg", "JPEG", "image/jpeg"),
15 | ("jp2", "JP2OpenJPEG", "image/jp2"),
16 | ("webp", "WEBP", "image/webp"),
17 | ("pngraw", "PNG", "image/png"),
18 | ],
19 | )
20 | def test_rasterformat(value, driver, mimetype):
21 | """Test driver and mimetype values."""
22 | assert RasterFormat[value].driver == driver
23 | assert RasterFormat[value].mediatype == mimetype
24 |
25 |
26 | @pytest.mark.parametrize(
27 | "value,driver,mimetype",
28 | [("pbf", "", "application/x-protobuf"), ("mvt", "", "application/x-protobuf")],
29 | )
30 | def test_vectorformat(value, driver, mimetype):
31 | """Test driver and mimetype values."""
32 | assert VectorTileFormat[value].driver == driver
33 | assert VectorTileFormat[value].mediatype == mimetype
34 |
--------------------------------------------------------------------------------