diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index 0c5332639..000000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,75 +0,0 @@ -[bumpversion] -current_version = 0.11.7 -commit = True -tag = True -tag_name = {new_version} -parse = - (?P\d+)\.(?P\d+)\.(?P\d+) - ((?P
a|b|rc)(?P\d+))?
-serialize = 
-	{major}.{minor}.{patch}{pre}{prenum}
-	{major}.{minor}.{patch}
-
-[bumpversion:file (global):pyproject.toml]
-search = version="{current_version}"
-replace = version="{new_version}"
-
-[bumpversion:file (core):pyproject.toml]
-search = titiler.core=={current_version}
-replace = titiler.core=={new_version}
-
-[bumpversion:file (extensions):pyproject.toml]
-search = titiler.extensions=={current_version}
-replace = titiler.extensions=={new_version}
-
-[bumpversion:file (mosaic):pyproject.toml]
-search = titiler.mosaic=={current_version}
-replace = titiler.mosaic=={new_version}
-
-[bumpversion:file (application):pyproject.toml]
-search = titiler.application=={current_version}
-replace = titiler.application=={new_version}
-
-[bumpversion:file:src/titiler/core/titiler/core/__init__.py]
-search = __version__ = "{current_version}"
-replace = __version__ = "{new_version}"
-
-[bumpversion:file:src/titiler/extensions/titiler/extensions/__init__.py]
-search = __version__ = "{current_version}"
-replace = __version__ = "{new_version}"
-
-[bumpversion:file:src/titiler/mosaic/titiler/mosaic/__init__.py]
-search = __version__ = "{current_version}"
-replace = __version__ = "{new_version}"
-
-[bumpversion:file:src/titiler/application/titiler/application/__init__.py]
-search = __version__ = "{current_version}"
-replace = __version__ = "{new_version}"
-
-[bumpversion:file:src/titiler/mosaic/pyproject.toml]
-search = titiler.core=={current_version}
-replace = titiler.core=={new_version}
-
-[bumpversion:file:src/titiler/extensions/pyproject.toml]
-search = titiler.core=={current_version}
-replace = titiler.core=={new_version}
-
-[bumpversion:file (core):src/titiler/application/pyproject.toml]
-search = titiler.core=={current_version}
-replace = titiler.core=={new_version}
-
-[bumpversion:file (extensions):src/titiler/application/pyproject.toml]
-search = titiler.extensions[cogeo,stac]=={current_version}
-replace = titiler.extensions[cogeo,stac]=={new_version}
-
-[bumpversion:file (mosaic):src/titiler/application/pyproject.toml]
-search = titiler.mosaic=={current_version}
-replace = titiler.mosaic=={new_version}
-
-[bumpversion:file:deployment/aws/lambda/Dockerfile]
-search = titiler.application=={current_version}
-replace = titiler.application=={new_version}
-
-[bumpversion:file:deployment/k8s/charts/Chart.yaml]
-search = appVersion: {current_version}
-replace = appVersion: {new_version}
diff --git a/.github/data/urls.txt b/.github/data/urls.txt
index 4782485da..40b810e3c 100644
--- a/.github/data/urls.txt
+++ b/.github/data/urls.txt
@@ -1,7 +1,7 @@
 PROT=http
 HOST=localhost
 PORT=8000
-PATH=cog/tiles/
+PATH=cog/tiles/WebMercatorQuad/
 EXT=.png
 QUERYSTRING=?url=/data/world.tif
 $(PROT)://$(HOST):$(PORT)/$(PATH)0/0/0$(EXT)$(QUERYSTRING)
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..4fa1bd99b
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,14 @@
+# Set update schedule for GitHub Actions
+
+version: 2
+updates:
+
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      # Check for updates to GitHub Actions every week
+      interval: "weekly"
+    groups:
+      all:
+        patterns:
+        - "*"
diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
index e2e9bd880..11a365980 100644
--- a/.github/workflows/benchmark.yml
+++ b/.github/workflows/benchmark.yml
@@ -11,17 +11,20 @@ on:
 
 jobs:
   benchmark:
+    if: github.repository == 'developmentseed/titiler'
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
 
       - name: install siege
         run: |
           sudo apt update
-          sudo apt install --yes siege
+          sudo apt install --yes siege jq
+          siege -C
+
       - name: Start containers
-        run: docker-compose -f "docker-compose.yml" up -d --build benchmark
+        run: docker compose -f "docker-compose.yml" up -d --build benchmark
 
       # Let's wait a bit to make sure the docker are up
       - name: Sleep for 10 seconds
@@ -30,12 +33,39 @@ jobs:
 
       - name: Run siege (WebMercator TMS)
         run: |
-          siege --file .github/data/urls.txt -b -c 1 -r 100 > /dev/null
+          siege --file .github/data/urls.txt -b -c 1 -r 100 --json-output 2>&1 | jq -c > results.json
+          echo "Benchmark Results"
+          cat results.json | jq
+          echo "Parse Results"
+          cat results.json | jq '{"name": "WebMercator elapsed_time", "unit": "s", "value": .elapsed_time}, {"name": "WebMercator data_transferred", "unit": "Megabytes", "value": .data_transferred}, {"name": "WebMercator response_time", "unit": "s", "value": .response_time}, {"name": "WebMercator longest_transaction", "unit": "s", "value": .longest_transaction}' > output.json
 
       - name: Run siege (WGS1984Quad TMS)
         run: |
-          siege --file .github/data/urls_wgs84.txt -b -c 1 -r 100 > /dev/null
+          siege --file .github/data/urls_wgs84.txt -b -c 1 -r 100 --json-output 2>&1 | jq -c > results.json
+          echo "Benchmark Results"
+          cat results.json | jq
+          echo "Parse Results"
+          cat results.json | jq '{"name": "WGS1984Quad elapsed_time", "unit": "s", "value": .elapsed_time}, {"name": "WGS1984Quad data_transferred", "unit": "Megabytes", "value": .data_transferred}, {"name": "WGS1984Quad response_time", "unit": "s", "value": .response_time}, {"name": "WGS1984Quad longest_transaction", "unit": "s", "value": .longest_transaction}' >> output.json
 
       - name: Stop containers
         if: always()
-        run: docker-compose -f "docker-compose.yml" down
+        run: docker compose -f "docker-compose.yml" down
+
+      - name: Merge Outputs
+        run: |
+          cat output.json | jq '[inputs]' > benchmark.json
+
+      - name: Check and Store benchmark result
+        uses: benchmark-action/github-action-benchmark@v1
+        with:
+          name: TiTiler performance Benchmarks
+          tool: 'customSmallerIsBetter'
+          output-file-path: benchmark.json
+          alert-threshold: '130%'
+          comment-on-alert: true
+          fail-on-alert: false
+          # GitHub API token to make a commit comment
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          gh-pages-branch: 'gh-benchmarks'
+          # Make a commit only if main
+          auto-push: ${{ github.ref == 'refs/heads/main' }}
diff --git a/.github/workflows/check_charts.yaml b/.github/workflows/check_charts.yaml
index e79f20937..467de546c 100644
--- a/.github/workflows/check_charts.yaml
+++ b/.github/workflows/check_charts.yaml
@@ -19,13 +19,13 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
 
       - name: Check Version
         run: |
-          current_version=$(grep 'version=' pyproject.toml | cut -f2 -d= | tr -d ' ' | tr -d '"')
+          current_version=$(grep '^version=' pyproject.toml | cut -f2 -d= | tr -d ' ' | tr -d '"')
           app_version=$(grep 'appVersion:' deployment/k8s/charts/Chart.yaml | cut -f2 -d: | tr -d ' ')
           if [[ "$current_version" != "$app_version" ]]; then
             echo "❌ current version from pyproject.toml ($current_version) and appVersion from Chart.yaml ($app_version) differs";
@@ -33,16 +33,16 @@ jobs:
           fi
 
       - name: Set up Helm
-        uses: azure/setup-helm@v1
+        uses: azure/setup-helm@v4
         with:
           version: v3.9.2
 
-      - uses: actions/setup-python@v2
+      - uses: actions/setup-python@v5
         with:
-          python-version: 3.7
+          python-version: '3.x'
 
       - name: Set up chart-testing
-        uses: helm/chart-testing-action@v2.2.1
+        uses: helm/chart-testing-action@v2.7.0
 
       - name: Run chart-testing (list-changed)
         id: list-changed
@@ -56,18 +56,18 @@ jobs:
         run: ct lint --chart-dirs deployment/k8s --target-branch ${{ github.event.repository.default_branch }}
 
       - name: Build container
-        uses: docker/build-push-action@v2
+        uses: docker/build-push-action@v6
         if: steps.list-changed.outputs.changed == 'true'
         with:
           # See https://github.com/developmentseed/titiler/discussions/387
           platforms: linux/amd64
           context: .
-          file: dockerfiles/Dockerfile.uvicorn
+          file: dockerfiles/Dockerfile
           push: false
           tags: "titiler:dev"
 
       - name: Create kind cluster
-        uses: helm/kind-action@v1.2.0
+        uses: helm/kind-action@v1.12.0
         if: steps.list-changed.outputs.changed == 'true'
 
       - name: Load container image in kind cluster
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d44a4ef80..3744f73e2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,30 +16,39 @@ on:
       - '.github/codecov.yml'
       - 'dockerfiles/**'
   pull_request:
+  workflow_dispatch:
+
 env:
-  LATEST_PY_VERSION: '3.10'
+  LATEST_PY_VERSION: '3.13'
 
 jobs:
   tests:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: ['3.8', '3.9', '3.10', '3.11']
+        python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
 
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
 
+      - name: Configure AWS credentials
+        uses: aws-actions/configure-aws-credentials@v4
+        with:
+          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+          aws-region: us-east-1
+
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
 
       - name: Test titiler.core
         run: |
-          python -m pip install -e src/titiler/core["test"]
+          python -m pip install -e src/titiler/core["test","telemetry"]
           python -m pytest src/titiler/core --cov=titiler.core --cov-report=xml --cov-append --cov-report=term-missing
 
       - name: Test titiler.extensions
@@ -47,6 +56,11 @@ jobs:
           python -m pip install -e src/titiler/extensions["test,cogeo,stac"]
           python -m pytest src/titiler/extensions --cov=titiler.extensions --cov-report=xml --cov-append --cov-report=term-missing
 
+      - name: Test titiler.xarray
+        run: |
+          python -m pip install -e src/titiler/xarray["test"]
+          python -m pytest src/titiler/xarray --cov=titiler.xarray --cov-report=xml --cov-append --cov-report=term-missing
+
       - name: Test titiler.mosaic
         run: |
           python -m pip install -e src/titiler/mosaic["test"]
@@ -65,7 +79,7 @@ jobs:
 
       - name: Upload Results
         if: ${{ matrix.python-version == env.LATEST_PY_VERSION }}
-        uses: codecov/codecov-action@v1
+        uses: codecov/codecov-action@v5
         with:
           file: ./coverage.xml
           flags: unittests
@@ -77,9 +91,9 @@ jobs:
     runs-on: ubuntu-latest
     if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release'
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v4
       - name: Set up Python
-        uses: actions/setup-python@v1
+        uses: actions/setup-python@v5
         with:
           python-version: ${{ env.LATEST_PY_VERSION }}
 
@@ -90,16 +104,16 @@ jobs:
 
       - name: Set tag version
         id: tag
-        # https://stackoverflow.com/questions/58177786/get-the-current-pushed-tag-in-github-actions
-        run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
+        run: |
+          echo "version=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
 
       - name: Set module version
         id: module
-        # https://stackoverflow.com/questions/58177786/get-the-current-pushed-tag-in-github-actions
-        run: echo ::set-output name=version::$(hatch --quiet version)
+        run: |
+          echo "version=$(hatch --quiet version)" >> $GITHUB_OUTPUT
 
       - name: Build and publish titiler packages
-        if: steps.tag.outputs.tag == steps.module.outputs.version
+        if: ${{ steps.tag.outputs.version }} == ${{ steps.module.outputs.version}}
         env:
           TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
           TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
@@ -112,96 +126,64 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
 
       - name: Set up QEMU
-        uses: docker/setup-qemu-action@v1
+        uses: docker/setup-qemu-action@v3
 
       - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v1
+        uses: docker/setup-buildx-action@v3
 
-      - name: Login to DockerHub
-        uses: docker/login-action@v1
+      - name: Login to Docker Hub
+        if: github.repository == 'developmentseed/titiler'
+        uses: docker/login-action@v3
         with:
           username: ${{ secrets.DOCKERHUB_USERNAME }}
           password: ${{ secrets.DOCKERHUB_TOKEN }}
 
-      - name: Login to Github
-        uses: docker/login-action@v1
+      - name: Log in to the GitHub Container registry
+        uses: docker/login-action@v3
         with:
           registry: ghcr.io
           username: ${{ github.actor }}
           password: ${{ secrets.GITHUB_TOKEN }}
 
-      - name: Set tag version
-        id: tag
-        # https://stackoverflow.com/questions/58177786/get-the-current-pushed-tag-in-github-actions
-        run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
-      # Uvicorn
-      # Push `latest` when commiting to main
-      - name: Build and push uvicorn
-        if: github.ref == 'refs/heads/main'
-        uses: docker/build-push-action@v2
+      - name: Docker meta
+        id: meta
+        uses: docker/metadata-action@v5
         with:
-          # See https://github.com/developmentseed/titiler/discussions/387
-          platforms: linux/amd64
-          context: .
-          file: dockerfiles/Dockerfile.uvicorn
-          push: true
-          tags: |
-            ghcr.io/${{ github.repository }}-uvicorn:latest
-
-      # Push `{VERSION}` when pushing a new tag
-      - name: Build and push uvicorn
-        if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release'
-        uses: docker/build-push-action@v2
-        with:
-          # See https://github.com/developmentseed/titiler/discussions/387
-          platforms: linux/amd64
-          context: .
-          file: dockerfiles/Dockerfile.uvicorn
-          push: true
-          tags: |
-            ghcr.io/${{ github.repository }}-uvicorn:${{ steps.tag.outputs.tag }}
-
-      # Gunicorn
-      # Push `latest` when commiting to main
-      - name: Build and push
-        if: github.ref == 'refs/heads/main'
-        uses: docker/build-push-action@v2
-        with:
-          # See https://github.com/developmentseed/titiler/discussions/387
-          platforms: linux/amd64
-          context: .
-          file: dockerfiles/Dockerfile.gunicorn
-          push: true
+          images: |
+            ghcr.io/${{ github.repository }}
+          flavor: |
+            latest=false
           tags: |
-            ghcr.io/${{ github.repository }}:latest
+            type=semver,pattern={{version}}
+            type=raw,value=latest,enable={{is_default_branch}}
 
-      # Push `{VERSION}` when pushing a new tag
       - name: Build and push
-        if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release'
-        uses: docker/build-push-action@v2
+        uses: docker/build-push-action@v6
         with:
-          # See https://github.com/developmentseed/titiler/discussions/387
-          platforms: linux/amd64
+          # TODO: add `linux/arm64 once https://github.com/rasterio/rasterio-wheels/issues/69 is resolved
+          platforms: linux/amd64 # ,linux/arm64
           context: .
-          file: dockerfiles/Dockerfile.gunicorn
-          push: true
-          tags: |
-            ghcr.io/${{ github.repository }}:${{ steps.tag.outputs.tag }}
+          file: dockerfiles/Dockerfile
+          push: ${{ github.event_name != 'pull_request' }}
+          tags: ${{ steps.meta.outputs.tags }}
+          labels: ${{ steps.meta.outputs.labels }}
+          cache-from: type=gha
+          cache-to: type=gha,mode=max
 
   deploy:
     needs: [tests, publish]
     runs-on: ubuntu-latest
-    if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release'
+    if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release' && github.repository == 'developmentseed/titiler'
 
     defaults:
       run:
         working-directory: deployment/aws
 
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       # Let's wait a bit to make sure Pypi is up to date
       - name: Sleep for 120 seconds
@@ -209,14 +191,14 @@ jobs:
         shell: bash
 
       - name: Configure AWS credentials
-        uses: aws-actions/configure-aws-credentials@v1
+        uses: aws-actions/configure-aws-credentials@v4
         with:
           aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
           aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
           aws-region: us-east-1
 
       - name: Set up Node.js
-        uses: actions/setup-node@v1
+        uses: actions/setup-node@v4
         with:
           node-version: '14.x'
 
@@ -224,7 +206,7 @@ jobs:
         run: npm install -g
 
       - name: Set up Python
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: '3.x'
 
diff --git a/.github/workflows/deploy_mkdocs.yml b/.github/workflows/deploy_mkdocs.yml
index f9b537459..a3a2e3db0 100644
--- a/.github/workflows/deploy_mkdocs.yml
+++ b/.github/workflows/deploy_mkdocs.yml
@@ -19,47 +19,19 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout main
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
 
-      - name: Set up Python 3.8
-        uses: actions/setup-python@v2
+      - name: Set up Python 3.11
+        uses: actions/setup-python@v5
         with:
-          python-version: 3.8
+          python-version: 3.11
 
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          python -m pip install src/titiler/core src/titiler/extensions["cogeo,stac"] src/titiler/mosaic src/titiler/application
-          python -m pip install nbconvert==6.5.3 mkdocs mkdocs-material mkdocs-jupyter pygments pdocs
+          python -m pip install src/titiler/core src/titiler/extensions["cogeo,stac"] src/titiler/xarray src/titiler/mosaic src/titiler/application
+          python -m pip install -r requirements/requirements-docs.txt
 
-      - name: update API docs
-        run: |
-          pdocs as_markdown \
-            --output_dir docs/src/api \
-            --exclude_source \
-            --overwrite \
-            titiler.core.dependencies \
-            titiler.core.factory \
-            titiler.core.routing \
-            titiler.core.errors \
-            titiler.core.resources.enums \
-            titiler.core.middleware
-
-          pdocs as_markdown \
-            --output_dir docs/src/api \
-            --exclude_source \
-            --overwrite \
-            titiler.extensions.cogeo \
-            titiler.extensions.viewer \
-            titiler.extensions.stac
-
-          pdocs as_markdown \
-            --output_dir docs/src/api \
-            --exclude_source \
-            --overwrite \
-            titiler.mosaic.factory \
-            titiler.mosaic.resources.enums \
-            titiler.mosaic.errors
 
       - name: Deploy docs
         run: mkdocs gh-deploy --force -f docs/mkdocs.yml
diff --git a/.gitignore b/.gitignore
index 95640297c..3cfc7f31b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -107,4 +107,4 @@ ENV/
 
 cdk.out/
 deployment/k8s/titiler/values-test.yaml
-docs/src/api/
+
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 63ed17680..0bbffeb17 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,26 +4,21 @@ repos:
     hooks:
       - id: validate-pyproject
 
-  - repo: https://github.com/psf/black
-    rev: 22.12.0
-    hooks:
-      - id: black
-        language_version: python
-
   - repo: https://github.com/PyCQA/isort
     rev: 5.12.0
     hooks:
       - id: isort
         language_version: python
 
-  - repo: https://github.com/charliermarsh/ruff-pre-commit
-    rev: v0.0.238
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.8.4
     hooks:
       - id: ruff
         args: ["--fix"]
+      - id: ruff-format
 
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v0.991
+    rev: v1.11.2
     hooks:
       - id: mypy
         language_version: python
@@ -31,3 +26,4 @@ repos:
         additional_dependencies:
         - types-simplejson
         - types-attrs
+        - pydantic~=2.0
diff --git a/CHANGES.md b/CHANGES.md
index 2481fa831..b09515124 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,840 @@
 # Release Notes
 
+## Unreleased
+
+### titiler.core
+
+* add OpenTelemetry instrumentation to the tiler factory classes
+
+### titiler.application
+
+* add OpenTelemetry tracing to the FastAPI application
+
+### Misc
+
+* Add otel-collector and jaeger to the docker network 
+
+## 0.22.4 (2025-07-02)
+
+* fix `rel` values for tiling scheme link (OGC Tiles specification)
+
+## 0.22.3 (2025-06-17)
+
+### titiler.xarray
+
+* use dimension's `dtype` to cast user *selection*
+
+## 0.22.2 (2025-06-02)
+
+### titiler.application
+
+* remove unused templates
+
+### titiler.xarray
+
+* fix `xarray_open_dataset` for cloud hosted files
+
+## 0.22.1 (2025-05-13)
+
+### titiler.xarray
+
+* update `reader` and `path_dependency` type informations
+
+## 0.22.0 (2025-05-06)
+
+### Misc
+
+* rename `/map` endpoint to `/map.html` **breaking change**
+* add `name` attribute to `BaseFactory` to define endpoint's `operationId`
+* add `operationId` on all endpoints
+* add `/preview/{width}x{height}.{format}` endpoints
+* update rio-tiler requirement to `>=7.7,<8.0`
+* allow users to pass only one of `width` or `heigh` size parameters for `preview`, `part` and `feature` requests
+* use `minZoom` instead of `minNativeZoom` in the `/map.html` html template
+* update geojson-pydantic requirement to `>=1.1.2,<3.0` and change featureCollection iteration
+
+### titiler.application
+
+* fix Landing page links when app is behind proxy
+* use `titiler.core` templates for Landing page
+* enable JSON and HTML rendering of the `/` landing page
+* add OGC Common `/conformance` endpoint
+
+### titiler.core
+
+* add `conforms_to` attribute to `BaseFactory` to indicate which conformance the TileFactory implement
+
+* remove deprecated `ColorFormulaParams` and `RescalingParams` dependencies **breaking change**
+
+* remove deprecated `DefaultDependency` dict-unpacking feature **breaking change**
+
+* add `min`, `max`, `mean`, `median`, `std` and `var` algorithms
+
+* Fix TerrainRGB algorithm and param user-controlled nodata-height (@jo-chemla, https://github.com/developmentseed/titiler/pull/1116)
+
+* add `output_min` and `output_max` metadata attributes to `slope` algorithm (@tayden, https://github.com/developmentseed/titiler/pull/1089)
+
+* add point value query on right-click to map viewer (@hrodmn, https://github.com/developmentseed/titiler/pull/1100)
+
+* refactor middlewares to use python's dataclasses
+
+* update `LoggerMiddleware` output format and options **breaking change**
+
+    ```python
+    from fastapi import FastAPI
+
+    from titiler.core.middlewares import LoggerMiddleware
+
+    # before
+    app = FastAPI()
+    app.add_middlewares(LoggerMiddleware, querystrings=True, headers=True)
+
+    # now
+    app = FastAPI()
+    app.add_middlewares(
+        LoggerMiddleware,
+        # custom Logger
+        logger=logging.getLogger("mytiler.requests"),  # default to logging.getLogger("titiler.requests")
+    )
+    ```
+
+    Note: logger needs then to be `configured` at runtime. e.g :
+
+    ```python
+    from logging import config
+    config.dictConfig(
+        {
+            "version": 1,
+            "disable_existing_loggers": False,
+            "formatters": {
+                "detailed": {
+                    "format": "%(asctime)s - %(levelname)s - %(name)s - %(message)s"
+                },
+                "request": {
+                    "format": (
+                        "%(asctime)s - %(levelname)s - %(name)s - %(message)s "
+                        + json.dumps(
+                            {
+                                k: f"%({k})s"
+                                for k in [
+                                    "method",
+                                    "referer",
+                                    "origin",
+                                    "route",
+                                    "path",
+                                    "path_params",
+                                    "query_params",
+                                    "headers",
+                                ]
+                            }
+                        )
+                    ),
+                },
+            },
+            "handlers": {
+                "console_request": {
+                    "class": "logging.StreamHandler",
+                    "level": "DEBUG",
+                    "formatter": "request",
+                    "stream": "ext://sys.stdout",
+                },
+            },
+            "loggers": {
+                "mytiler.requests": {
+                    "level": "INFO",
+                    "handlers": ["console_request"],
+                    "propagate": False,
+                },
+            },
+        }
+    )
+    ```
+
+### titiler.extensions
+
+* update `wms` extension to remove usage of `ColorFormulaParams` and `RescalingParams` dependencies
+* update `render` extension to better validate query-parameters from render expression
+
+### titiler.xarray
+
+* update `rio-tiler` requirement to `>=7.6.1`
+* add `sel` and `sel_method` options to select dimension
+
+    ```
+    # before
+    https://.../0/0/0.png?url=dataset.zarr&drop_dim=time=2023-01-01
+
+    # now
+    https://.../0/0/0.png?url=dataset.zarr&sel=time=2023-01-01
+
+    # method
+    https://.../0/0/0.png?url=dataset.zarr&sel=time=2023-01-02&sel_method=nearest
+
+    # Can use `slice` when providing 2 values
+    https://.../0/0/0.png?url=dataset.zarr&sel=time=2023-01-01&time=2023-01-31
+    ```
+* add support for `bidx` parameter
+* remove `first` **time** dim selection **breaking change**
+* add support for 3D dataset
+* remove `drop_dim` option **breaking change**
+* remove `datetime` option **breaking change**
+* deprecate `VariablesExtension` extension
+* add `DatasetMetadataExtension` extension (`/dataset/keys`, `/dataset/` and `/dataset/dict` endpoints)
+
+### titiler.mosaic
+
+* add `/bbox` prefix to `/{minx},{miny},{maxx},{maxy}/assets` endpoint -> `/bbox/{minx},{miny},{maxx},{maxy}/assets` **breaking change**
+* add `/point` prefix to `{lon},{lat}/assets` endpoint -> `/point/{lon},{lat}/assets` **breaking change**
+* add `/tiles` prefix to `/{tileMatrixSetId}/{z}/{x}/{y}/assets` endpoint -> `/tiles/{tileMatrixSetId}/{z}/{x}/{y}/assets` **breaking change**
+* add `assets_accessor_dependency` dependency to the MosaicTileFactory to pass options to the backend's `get_assets` method.
+
+## 0.21.1 (2025-01-29)
+
+### titiler.core
+
+* add `slope` algorithm (@tayden, https://github.com/developmentseed/titiler/pull/1088)
+
+### titiler.xarray
+
+* Support Zarr-Python >=3 (author @maxrjones, https://github.com/developmentseed/titiler/pull/1082)
+
+## 0.21.0 (2025-01-24)
+
+### Misc
+
+* use `URN` style CRS notation in WMTS document
+
+* Unify Docker images (deprecate `titiler-uvicorn`)
+
+    ```
+    # Uvicorn
+    # before
+    docker run \
+        --platform=linux/amd64 \
+        -p 8000:8000 \
+        --env PORT=8000 \
+        --rm -it ghcr.io/developmentseed/titiler-uvicorn:latest
+
+    # now
+    docker run \
+        --platform=linux/amd64 \
+        -p 8000:8000 \
+        --rm -it ghcr.io/developmentseed/titiler:latest \
+        uvicorn titiler.application.main:app --host 0.0.0.0 --port 8000 --workers 1
+
+    # Gunicorn
+    # before
+    docker run \
+        --platform=linux/amd64 \
+        -p 8000:8000 \
+        --env PORT=8000 \
+        --rm -it ghcr.io/developmentseed/titiler:latest
+
+    # now
+    docker run \
+        --platform=linux/amd64 \
+        -p 8000:8000 \
+        --rm -it ghcr.io/developmentseed/titiler:latest \
+        gunicorn -k uvicorn.workers.UvicornWorker titiler.application.main:app --bind 0.0.0.0:8000 --workers 1
+    ```
+
+## 0.20.1 (2025-01-09)
+
+### titiler.xarray
+
+* pin python `zarr` to `>2,<3.0` to avoid zarr 3.0 breaking changes
+
+## 0.20.0 (2025-01-07)
+
+### titiler.core
+
+* add layer control to map viewer template (author @hrodmn, https://github.com/developmentseed/titiler/pull/1051)
+* improve query string handling in LowerCaseQueryStringMiddleware using urlencode (author @pratapvardhan, https://github.com/developmentseed/titiler/pull/1050)
+* add `titiler.core.utils.bounds_to_geometry` and reduce code duplication in factories (author @PratapVardhan, https://github.com/developmentseed/titiler/pull/1047)
+* simplify image format dtype validation in `render_image` (author @PratapVardhan, https://github.com/developmentseed/titiler/pull/1046)
+* remove `rescale_dependency` and `color_formula_dependency` attributes in `TilerFactory` class  **breaking change**
+* move `rescale` and `color_formula` QueryParameters dependencies in `ImageRenderingParams` class  **breaking change**
+* handle image rescaling and color_formula within `titiler.core.utils.render_image` function  **breaking change**
+* add `render_func: Callable[..., Tuple[bytes, str]] = render_image` attribute in `TilerFactory` class
+* add `castToInt`, `Floor`, `Ceil` algorithms
+
+### titiler.application
+
+* update `/healthz` endpoint to return dependencies versions (titiler, rasterio, gdal, ...) (author @scottyhq, https://github.com/developmentseed/titiler/pull/1056)
+* migrate `templates/index.html` to bootstrap5, remove unused css, reuse bs classes (author @PratapVardhan, https://github.com/developmentseed/titiler/pull/1048)
+
+### titiler.mosaic
+
+* remove `rescale_dependency` and `color_formula_dependency` attributes in `MosaicTilerFactory` class  **breaking change**
+* add `render_func: Callable[..., Tuple[bytes, str]] = render_image` attribute in `MosaicTilerFactory` class  **breaking change**
+
+### titiler.extensions
+
+* use `factory.render_func` as render function in `wmsExtension` endpoints
+* add `stacRenderExtension` which adds two endpoints: `/renders` (lists all renders) and `/renders/` (render metadata and links) (author @alekzvik, https://github.com/developmentseed/titiler/pull/1038)
+
+### Misc
+
+* Updated WMTS Capabilities template to avoid inserting extra new lines (author @AndrewAnnex, https://github.com/developmentseed/titiler/pull/1052).
+* Updated WMTS endpoint in titiler.mosaic and titiler.core to return layer bounds in coordinate ordering matching CRS order if WGS84 is not used (author @AndrewAnnex, https://github.com/developmentseed/titiler/pull/1052).
+* Remove `python3.8` support (author @pratapvardhan, https://github.com/developmentseed/titiler/pull/1058)
+* Add `python3.13` support (author @pratapvardhan, https://github.com/developmentseed/titiler/pull/1058)
+
+## 0.19.3 (2025-01-09)
+
+### titiler.xarray
+
+* pin python zarr to >2,<3.0 to avoid zarr 3.0 breaking changes [Backported from 0.20.1]
+
+
+## 0.19.2 (2024-11-28)
+
+### Misc
+
+* drop python 3.8 and add python 3.13 support (author @pratapvardhan, https://github.com/developmentseed/titiler/pull/1058)
+
+* Update package build backend from `pdm-pep517` to `pdm-backend` (https://backend.pdm-project.org/#migrate-from-pdm-pep517)
+
+* Update namespace package from using `.` to `-` as separator to comply with PEP-625 (https://peps.python.org/pep-0625/)
+
+### titiler.mosaic
+
+* Define variable (`MOSAIC_CONCURRENCY` and `MOSAIC_STRICT_ZOOM`) from env-variable outside endpoint code
+
+## 0.19.1 (2024-11-14)
+
+* Add `titiler` links in Map attributions
+
+## 0.19.0 (2024-11-07)
+
+### Misc
+
+* Remove default `WebMercatorQuad` tile matrix set in `/tiles`, `/tilesjson.json`, `/map` and `/WMTSCapabilities.xml` endpoints **breaking change**
+
+    ```
+    # Before
+    /tiles/{z}/{x}/{y}
+    /tilejson.json
+    /map
+    /WMTSCapabilities.xml
+
+    # Now
+    /tiles/WebMercatorQuad/{z}/{x}/{y}
+    /WebMercatorQuad/tilejson.json
+    /WebMercatorQuad/map
+    /WebMercatorQuad/WMTSCapabilities.xml
+    ```
+
+* Use `@attrs.define` instead of dataclass for factories **breaking change**
+* Use `@attrs.define` instead of dataclass for factory extensions **breaking change**
+* Handle `numpy` types in JSON/GeoJSON response
+* In the `map.html` template, use the tilejson's `minzoom` and `maxzoom` to populate `minNativeZoom` and `maxNativeZoom` parameters in leaflet `tileLayer` instead of `minZoom` and `maxZoom`
+
+### titiler.core
+
+* Update `rio-tiler` dependency to `>=7.0,<8.0`
+
+* Update `geojson-pydantic` dependency to `>=1.1.2,<2.0` which better handle antimeridian crossing dataset
+
+* handle `antimeridian` crossing bounds in `/info.geojson` endpoints (returning MultiPolygon instead of Polygon)
+
+* Improve XSS security for HTML templates (author @jcary741, https://github.com/developmentseed/titiler/pull/953)
+
+* Remove all default values to the dependencies **breaking change**
+
+    * `DatasetParams.unscale`: `False` -> `None` (default to `False` in rio-tiler)
+    * `DatasetParams.resampling_method`: `nearest` -> `None` (default to `nearest` in rio-tiler)
+    * `DatasetParams.reproject_method`: `nearest` -> `None` (default to `nearest` in rio-tiler)
+    * `ImageRenderingParams.add_mask`: `True` -> `None` (default to `True` in rio-tiler)
+    * `StatisticsParams.categorical`: `False` -> `None` (default to `False` in rio-tiler)
+
+* Add `as_dict(exclude_none=True/False)` method to the `DefaultDependency` class.
+
+    ```python
+    from typing import Optional
+    from titiler.core.dependencies import DefaultDependency
+    from dataclasses import dataclass
+
+    @dataclass
+    class Deps(DefaultDependency):
+        value: Optional[int] = None
+
+    print({**Deps().__dict__.items()})
+    >> {'value': None}
+
+    Deps().as_dict()  # `exclude_none` defaults to True
+    >> {}
+
+    Deps(value=1).as_dict()
+    >> {'value': 1}
+    ```
+
+* Fix Hillshade algorithm (bad `azimuth` angle)
+
+* Set default `azimuth` and `altitude` angles to 45º for the Hillshade algorithm **breaking change**
+
+* Use `.as_dict()` method when passing option to rio-tiler Reader's methods to avoid parameter conflicts when using custom Readers.
+
+* Rename `BaseTilerFactory` to `BaseFactory` **breaking change**
+
+* Remove useless attribute in `BaseFactory` (and moved them to `TilerFactory`) **breaking change**
+
+* Add `crs` option to `/bounds` endpoints to enable geographic_crs selection by the user
+
+* `/bounds` endpoints now return a `crs: str` attribute in the response
+
+* update `wmts.xml` template to support multiple layers
+
+* re-order endpoints parameters
+
+* avoid `lat/lon` overflow in `map` viewer
+
+* add OGC Tiles `/tiles` and `/tiles/{tileMatrixSet}` endpoints
+
+* add `gif` media type
+
+* `/point` endpoint returned masked values (`None` is nodata)
+
+### titiler.mosaic
+
+* Rename `reader` attribute to `backend` in `MosaicTilerFactory`  **breaking change**
+
+* Add `crs` option to `/bounds` endpoints to enable geographic_crs selection by the user
+
+* `/bounds` endpoints now return a `crs: str` attribute in the response
+
+* Update `cogeo-mosaic` dependency to `>=8.0,<9.0`
+
+* re-order endpoints parameters
+
+* add OGC Tiles `/tiles` and `/tiles/{tileMatrixSet}` endpoints
+
+* `/point` endpoint returned masked values (`None` is nodata)
+
+### titiler.extensions
+
+* Encode URL for cog_viewer and stac_viewer (author @guillemc23, https://github.com/developmentseed/titiler/pull/961)
+
+* Add links for render parameters and `/map` link to **viewer** dashboard (author @hrodmn, https://github.com/developmentseed/titiler/pull/987)
+
+* Update viewers to use `/info.geojson` endpoint instead of `/info`
+
+## 0.18.10 (2024-10-17)
+
+### titiler.application
+
+* update `starlette-cramjam` dependency and set compression-level default to `6`
+
+## 0.18.9 (2024-09-23)
+
+* fix release 0.18.8
+
+## 0.18.8 (2024-09-23)
+
+### titiler.extensions
+
+* Add links for render parameters and /map link to viewer dashboard (author @hrodmn, https://github.com/developmentseed/titiler/pull/987)
+
+## 0.18.7 (2024-09-19)
+
+* fix Hillshade algorithm (bad `azimuth` angle) (https://github.com/developmentseed/titiler/pull/985) [Backported]
+* Encode URL for cog_viewer and stac_viewer (author @guillemc23, https://github.com/developmentseed/titiler/pull/961) [Backported]
+* Improve XSS security for HTML templates (author @jcary741, https://github.com/developmentseed/titiler/pull/953) [Backported]
+
+## 0.18.6 (2024-08-27)
+
+* Switch back to `fastapi` instead of `fastapi-slim` and use `>=0.109.0` version
+
+## 0.18.5 (2024-07-03)
+
+* Set version requirement for FastAPI to `>=0.111.0`
+
+## 0.18.4 (2024-06-26)
+
+* fix Tiles URL encoding for WMTSCapabilities XML document
+
+## 0.18.3 (2024-05-20)
+
+* fix `WMTSCapabilities.xml` response for ArcMap compatibility
+    * replace `Cloud Optimized GeoTIFF` with dataset URL or `TiTiler` for the *ows:ServiceIdentification* **title**
+    * replace `cogeo` with `Dataset` for the `layer` *ows:Identifier*
+
+## 0.18.2 (2024-05-07)
+
+* move to `fastapi-slim` to avoid unwanted dependencies (author @n8sty, https://github.com/developmentseed/titiler/pull/815)
+
+## 0.18.1 (2024-04-12)
+
+### titiler.core
+
+* fix `TerrainRGB` algorithm name (author @JinIgarashi, https://github.com/developmentseed/titiler/pull/804)
+* add more tests for `RescalingParams` and `HistogramParams` dependencies
+* make sure to return *empty* content for `204` Error code
+
+## 0.18.0 (2024-03-22)
+
+### titiler.core
+
+* Add `ColorMapFactory` to create colorMap metadata endpoints (https://github.com/developmentseed/titiler/pull/796)
+* **Deprecation** remove default `WebMercatorQuad` tile matrix set in `/tiles`, `/tilesjson.json`, `/map` and `/WMTSCapabilities.xml` endpoints (https://github.com/developmentseed/titiler/pull/802)
+
+    ```
+    # Before
+    /tiles/{z}/{x}/{y}
+    /tilejson.json
+    /map
+    /WMTSCapabilities.xml
+
+    # Now
+    /tiles/WebMercatorQuad/{z}/{x}/{y}
+    /WebMercatorQuad/tilejson.json
+    /WebMercatorQuad/map
+    /WebMercatorQuad/WMTSCapabilities.xml
+    ```
+
+* **Deprecation** `default_tms` attribute in `BaseTilerFactory` (because `tileMatrixSetId` is now required in endpoints).
+
+### titiler.mosaic
+
+* **Deprecation** remove default `WebMercatorQuad` tile matrix set in `/tiles`, `/tilesjson.json`, `/map` and `/WMTSCapabilities.xml` endpoints (https://github.com/developmentseed/titiler/pull/802)
+
+    ```
+    # Before
+    /tiles/{z}/{x}/{y}
+    /tilejson.json
+    /map
+    /WMTSCapabilities.xml
+
+    # Now
+    /tiles/WebMercatorQuad/{z}/{x}/{y}
+    /WebMercatorQuad/tilejson.json
+    /WebMercatorQuad/map
+    /WebMercatorQuad/WMTSCapabilities.xml
+    ```
+
+* **Deprecation** `default_tms` attribute in `MosaicTilerFactory` (because `tileMatrixSetId` is now required in endpoints).
+
+### Misc
+
+* add `request` as first argument in `TemplateResponse` to adapt with latest starlette version
+
+## 0.17.3 (2024-03-21)
+
+### titiler.application
+
+* Add `extra="ignore"` option `ApiSettings` to fix pydantic issue when using `.env` file (author @imanshafiei540, https://github.com/developmentseed/titiler/pull/800)
+
+## 0.17.2 (2024-03-15)
+
+### titiler.core
+
+* fix OpenAPI metadata for algorithm (author @JinIgarashi, https://github.com/developmentseed/titiler/pull/797)
+
+## 0.17.1 (2024-03-13)
+
+* add python 3.12 support
+
+### titiler.core
+
+* Add `use_epsg` parameter to WMTS endpoint to resolve ArcMAP issues and fix XML formating (author @gadomski, https://github.com/developmentseed/titiler/pull/782)
+* Add more OpenAPI metadata for algorithm (author @JinIgarashi, https://github.com/developmentseed/titiler/pull/783)
+
+### titiler.application
+
+* fix invalid url parsing in HTML responses
+
+## 0.17.0 (2024-01-17)
+
+### titiler.core
+
+* update `rio-tiler` version to `>6.3.0`
+* use new `align_bounds_with_dataset=True` rio-tiler option in GeoJSON statistics methods for more precise calculation
+
+## 0.16.2 (2024-01-17)
+
+### titiler.core
+
+* fix leafletjs template maxZoom to great than 18 for `/map` endpoint (author @Firefishy, https://github.com/developmentseed/titiler/pull/749)
+
+## 0.16.1 (2024-01-08)
+
+### titiler.core
+
+* use morecantile `TileMatrixSet.cellSize` property instead of deprecated/private `TileMatrixSet._resolution` method
+
+### titiler.mosaic
+
+* use morecantile `TileMatrixSet.cellSize` property instead of deprecated/private `TileMatrixSet._resolution` method
+
+## 0.16.0 (2024-01-08)
+
+### titiler.core
+
+* update FastAPI version lower limit to `>=0.107.0`
+* fix template loading for starlette >= 0.28 by using `jinja2.Environment` argument (author @jasongi, https://github.com/developmentseed/titiler/pull/744)
+
+### titiler.extensions
+
+* fix template loading for starlette >= 0.28 by using `jinja2.Environment` argument (author @jasongi, https://github.com/developmentseed/titiler/pull/744)
+
+### titiler.application
+
+* fix template loading for starlette >= 0.28 by using `jinja2.Environment` argument (author @jasongi, https://github.com/developmentseed/titiler/pull/744)
+
+## 0.15.8 (2024-01-08)
+
+### titiler.core
+
+* use morecantile `TileMatrixSet.cellSize` property instead of deprecated/private `TileMatrixSet._resolution` method [backported from 0.16.1]
+
+### titiler.mosaic
+
+* use morecantile `TileMatrixSet.cellSize` property instead of deprecated/private `TileMatrixSet._resolution` method [backported from 0.16.1]
+
+## 0.15.7 (2024-01-08)
+
+### titiler.core
+
+* update FastAPI version upper limit to `<0.107.0` to avoid starlette breaking change (`0.28`)
+
+### titiler.application
+
+* add simple *auth* (optional) based on `global_access_token` string, set with `TITILER_API_GLOBAL_ACCESS_TOKEN` environment variable (author @DeflateAwning, https://github.com/developmentseed/titiler/pull/735)
+
+## 0.15.6 (2023-11-16)
+
+### titiler.core
+
+* in `/map` HTML response, add Lat/Lon buffer to AOI to avoid creating wrong AOI (when data covers the whole world).
+
+## 0.15.5 (2023-11-09)
+
+### titiler.core
+
+* add `algorithm` options for `/statistics` endpoints
+
+* switch from `BaseReader.statistics()` method to a combination of `BaseReader.preview()` and `ImageData.statistics()` methods to get the statistics
+
+## 0.15.4 (2023-11-06)
+
+### titiler.core
+
+* update `rio-tiler` requirement to `>=6.2.5,<7.0`
+
+* allow `bidx` option in `titiler.core.dependencies.AssetsBidxExprParams` and `titiler.core.dependencies.AssetsBidxParams`
+
+    ```python
+    # merge band 1 form asset1 and asset2
+    # before
+    httpx.get(
+        "/stac/preview",
+        params=(
+            ("url", "stac.json"),
+            ("assets", "asset1"),
+            ("assets", "asset2"),
+            ("asset_bidx", "asset1|1"),
+            ("asset_bidx", "asset2|1"),
+        )
+    )
+
+    # now
+    httpx.get(
+        "/stac/preview",
+        params=(
+            ("url", "stac.json"),
+            ("assets", "asset1"),
+            ("assets", "asset2"),
+            ("bidx", 1),
+        )
+    )
+    ```
+
+* fix openapi examples
+
+## 0.15.3 (2023-11-02)
+
+* add `dst_crs` options in `/statistics [POST]` and `/feature [POST]` endpoints
+
+## 0.15.2 (2023-10-23)
+
+### titiler.core
+
+* add `dependencies.TileParams` dependency with `buffer` and `padding` options
+* add `tile_dependency` attribute in `TilerFactory` class (defaults to `TileParams`)
+* add `reproject` (alias to `reproject_method`) option in `DatasetParams` dependency
+
+### titiler.mosaic
+
+*  Change `HTTP_404_NOT_FOUND` to `HTTP_204_NO_CONTENT` when no asset is found or tile is empty (author @simouel, https://github.com/developmentseed/titiler/pull/713)
+* add `tile_dependency` attribute in `MosaicTilerFactory` class (defaults to `TileParams`)
+
+### cdk application
+
+* Support non-root paths in AWS API Gateway Lambda handler (author @DanSchoppe, https://github.com/developmentseed/titiler/pull/716)
+
+## 0.15.1 (2023-10-17)
+
+* Allow a default `color_formula` parameter to be set via a dependency (author @samn, https://github.com/developmentseed/titiler/pull/707)
+* add `titiler.core.dependencies.create_colormap_dependency` to create ColorMapParams dependency from `rio_tiler.colormap.ColorMaps` object
+* add `py.typed` files in titiler submodules (https://peps.python.org/pep-0561)
+
+## 0.15.0 (2023-09-28)
+
+### titiler.core
+
+- added `PartFeatureParams` dependency
+
+**breaking changes**
+
+- `max_size` is now set to `None` for `/statistics [POST]`, `/bbox` and `/feature` endpoints, meaning the tiler will create image from the highest resolution.
+
+- renamed `titiler.core.dependencies.ImageParams` to `PreviewParams`
+
+- split TileFactory `img_dependency` attribute in two:
+  - `img_preview_dependency`: used in `/preview` and `/statistics [GET]`, default to `PreviewParams` (with `max_size=1024`)
+
+  - `img_part_dependency`: used in `/bbox`, `/feature` and `/statistics [POST]`, default to `PartFeatureParams` (with `max_size=None`)
+
+- renamed `/crop` endpoints to `/bbox/...` or `/feature/...`
+  - `/crop/{minx},{miny},{maxx},{maxy}.{format}` -> `/bbox/{minx},{miny},{maxx},{maxy}.{format}`
+
+  - `/crop/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}` -> `/bbox/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}`
+
+  - `/crop [POST]` -> `/feature [POST]`
+
+  - `/crop.{format} [POST]` -> `/feature.{format} [POST]`
+
+  - `/crop/{width}x{height}.{format}  [POST]` -> `/feature/{width}x{height}.{format} [POST]`
+
+- update `rio-tiler` requirement to `>=6.2.1`
+
+- Take coverage weights in account when generating statistics from GeoJSON features
+
+## 0.14.1 (2023-09-14)
+
+### titiler.extension
+
+* add `GetFeatureInfo` capability in `wmsExtension` (author @benjaminleighton, https://github.com/developmentseed/titiler/pull/698)
+
+## 0.14.0 (2023-08-30)
+
+### titiler.core
+
+* replace `-` by `_` in query parameters **breaking change**
+  - `coord-crs` -> `coord_crs`
+  - `dst-crs` -> `dst_crs`
+
+* replace `buffer` and `color_formula` endpoint parameters by external dependencies (`BufferParams` and `ColorFormulaParams`)
+
+* add `titiler.core.utils.render_image` which allow non-binary alpha band created with custom colormap. `render_image` replace `ImageData.render` method.
+
+    ```python
+    # before
+    if cmap := colormap or dst_colormap:
+        image = image.apply_colormap(cmap)
+
+    if not format:
+        format = ImageType.jpeg if image.mask.all() else ImageType.png
+
+    content = image.render(
+        img_format=format.driver,
+        **format.profile,
+        **render_params,
+    )
+
+    # now
+    # render_image will:
+    # - apply the colormap
+    # - choose the right output format if `None`
+    # - create the binary data
+    content, media_type = render_image(
+        image,
+        output_format=format,
+        colormap=colormap or dst_colormap,
+        **render_params,
+    )
+    ```
+
+### titiler.extension
+
+* rename `geom-densify-pts` to `geometry_densify` **breaking change**
+* rename `geom-precision` to `geometry_precision` **breaking change**
+
+## 0.13.3 (2023-08-27)
+
+* fix Factories `url_for` method and avoid changing `Request.path_params` object
+
+## 0.13.2 (2023-08-24)
+
+### titiler.extensions
+
+* replace mapbox-gl by maplibre
+* replace Stamen by OpenStreetMap tiles
+* simplify band selection handling (author @tayden, https://github.com/developmentseed/titiler/pull/688)
+
+## 0.13.1 (2023-08-21)
+
+### titiler.core
+
+* fix `LowerCaseQueryStringMiddleware` unexpectedly truncating query parameters (authors @jthetzel and @jackharrhy, @https://github.com/developmentseed/titiler/pull/677)
+
+## titiler.application
+
+* add `cors_allow_methods` in `ApiSettings` to control the CORS allowed methods (author @ubi15, https://github.com/developmentseed/titiler/pull/684)
+
+## 0.13.0 (2023-07-27)
+
+* update core requirements to libraries using pydantic **~=2.0**
+
+### titiler.core
+
+* update requirements:
+  * fastapi `>=0.95.1` --> `>=0.100.0`
+  * pydantic `~=1.0` --> `~=2.0`
+  * rio-tiler `>=5.0,<6.0` --> `>=6.0,<7.0`
+  * morecantile`>=4.3,<5.0` --> `>=5.0,<6.0`
+  * geojson-pydantic `>=0.4,<0.7` --> `>=1.0,<2.0`
+  * typing_extensions `>=4.6.1`
+
+### titiler.extension
+
+* update requirements:
+  * rio-cogeo `>=4.0,<5.0"` --> `>=5.0,<6.0"`
+
+### titiler.mosaic
+
+* update requirements:
+  * cogeo-mosaic `>=6.0,<7.0` --> `>=7.0,<8.0`
+
+### titiler.application
+
+* use `/api` and `/api.html` for documentation (instead of `/openapi.json` and `/docs`)
+* update landing page
+
+## 0.12.0 (2023-07-17)
+
+* use `Annotated` Type for Query/Path parameters
+* replace variable `TileMatrixSetId` by `tileMatrixSetId`
+
+### titiler.core
+
+* update FastAPI dependency to `>=0.95.1`
+* set `pydantic` dependency to `~=1.0`
+* update `rio-tiler` dependency to `>=5.0,<6.0`
+* update TMS endpoints to match OGC Tiles specification
+
+### titiler.extensions
+
+* use TiTiler's custom JSONResponse for the `/validate` endpoint to avoid issue when COG has `NaN` nodata value
+* update `rio-cogeo` dependency to `>=4.0,<5.0`
+* update `rio-stac` requirement to `>=0.8,<0.9` and add `geom-densify-pts` and `geom-precision` options
+
+## titiler.mosaic
+
+* update `cogeo-mosaic` dependency to `>=6.0,<7.0`
+* remove `titiler.mosaic.resources.enum.PixelSelectionMethod` and use `rio_tiler.mosaic.methods.PixelSelectionMethod`
+* allow more TileMatrixSet (than only `WebMercatorQuad`)
+
 ## 0.11.7 (2023-05-18)
 
 ### titiler.core
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a7105795c..6357bacce 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -47,7 +47,7 @@ python -m pytest src/titiler/application --cov=titiler.application --cov-report=
 ```bash
 git clone https://github.com/developmentseed/titiler.git
 cd titiler
-python -m pip install nbconvert mkdocs mkdocs-material mkdocs-jupyter pygments pdocs
+python -m pip install -r requirements/requirements-docs.txt
 ```
 
 Hot-reloading docs:
@@ -63,32 +63,22 @@ Actions deploys automatically for new commits.):
 mkdocs gh-deploy -f docs/mkdocs.yml
 ```
 
-```bash
-pdocs as_markdown \
-   --output_dir docs/src/api \
-   --exclude_source \
-   --overwrite \
-   titiler.core.dependencies \
-   titiler.core.factory \
-   titiler.core.utils \
-   titiler.core.routing \
-   titiler.core.errors \
-   titiler.core.resources.enums \
-   titiler.core.middleware
-
-pdocs as_markdown \
-   --output_dir docs/src/api \
-   --exclude_source \
-   --overwrite \
-   titiler.extensions.cogeo \
-   titiler.extensions.viewer \
-   titiler.extensions.stac
-
-pdocs as_markdown \
-   --output_dir docs/src/api \
-   --exclude_source \
-   --overwrite \
-   titiler.mosaic.factory \
-   titiler.mosaic.resources.enums \
-   titiler.mosaic.errors
-```
+### Release
+
+This is a checklist for releasing a new version of **titiler**.
+
+1. Create a release branch named `release/vX.Y.Z`, where `X.Y.Z` is the new version
+
+2. Make sure the [Changelog](CHANGES.md) is up to date with latest changes and release date set
+
+3. Update `version: {chart_version}` (e.g: `version: 1.1.6 -> version: 1.1.7`) in `deployment/k8s/charts/Chart.yaml`
+
+4. Run [`bump-my-version`](https://callowayproject.github.io/bump-my-version/) to update all titiler's module versions: `bump-my-version bump minor --new-version 0.20.0`
+
+5. Push your release branch, create a PR, and get approval
+
+6. Once the PR is merged, create a new (annotated, signed) tag on the appropriate commit. Name the tag `X.Y.Z`, and include `vX.Y.Z` as its annotation message
+
+7. Push your tag to Github, which will kick off the publishing workflow
+
+8. Create a [new release](https://github.com/developmentseed/titiler/releases/new) targeting the new tag, and use the "Generate release notes" feature to populate the description. Publish the release and mark it as the latest
diff --git a/README.md b/README.md
index 44f4aed17..8eb17caba 100644
--- a/README.md
+++ b/README.md
@@ -26,13 +26,13 @@
 
 ---
 
-**Documentation**: https://devseed.com/titiler/
+**Documentation**: https://devseed.com/titiler/
 
 **Source Code**: https://github.com/developmentseed/titiler
 
 ---
 
-`Titiler`, pronounced **tee-tiler** (*ti* is the diminutive version of the french *petit* which means small), is a set of python modules that focus on creating FastAPI application for dynamic tiling.
+`TiTiler`, pronounced **tee-tiler** (*ti* is the diminutive version of the french *petit* which means small), is a set of python modules that focus on creating FastAPI application for dynamic tiling.
 
 Note: This project is the descendant of [`cogeo-tiler`](https://github.com/developmentseed/cogeo-tiler) and [`cogeo-mosaic-tiler`](https://github.com/developmentseed/cogeo-mosaic-tiler).
 
@@ -42,6 +42,7 @@ Note: This project is the descendant of [`cogeo-tiler`](https://github.com/devel
 - [Cloud Optimized GeoTIFF](http://www.cogeo.org/) support
 - [SpatioTemporal Asset Catalog](https://stacspec.org) support
 - Multiple projections support (see [TileMatrixSets](https://www.ogc.org/standards/tms)) via [`morecantile`](https://github.com/developmentseed/morecantile).
+- MultiDimensional dataset support via [Xarray](https://github.com/pydata/xarray)
 - JPEG / JP2 / PNG / WEBP / GTIFF / NumpyTile output format support
 - OGC WMTS support
 - Automatic OpenAPI documentation (FastAPI builtin)
@@ -55,6 +56,7 @@ Starting with version `0.3.0`, the `TiTiler` python module has been split into a
 | Package | Version |  Description
 | ------- | ------- |-------------
 [**titiler.core**](https://github.com/developmentseed/titiler/tree/main/src/titiler/core) | [![titiler.core](https://img.shields.io/pypi/v/titiler.core?color=%2334D058&label=pypi)](https://pypi.org/project/titiler.core) | The `Core` package contains libraries to help create a  dynamic tiler for COG and STAC
+[**titiler.xarray**](https://github.com/developmentseed/titiler/tree/main/src/titiler/xarray) | [![titiler.xarray](https://img.shields.io/pypi/v/titiler.xarray?color=%2334D058&label=pypi)](https://pypi.org/project/titiler.xarray) | The `xarray` package contains libraries to help create a  dynamic tiler for Zarr/NetCDF datasets
 [**titiler.extensions**](https://github.com/developmentseed/titiler/tree/main/src/titiler/extensions) | [![titiler.extensions](https://img.shields.io/pypi/v/titiler.extensions?color=%2334D058&label=pypi)](https://pypi.org/project/titiler.extensions) | TiTiler's extensions package. Contains extensions for Tiler Factories.
 [**titiler.mosaic**](https://github.com/developmentseed/titiler/tree/main/src/titiler/mosaic) | [![titiler.mosaic](https://img.shields.io/pypi/v/titiler.mosaic?color=%2334D058&label=pypi)](https://pypi.org/project/titiler.mosaic) | The `mosaic` package contains libraries to help create a dynamic tiler for MosaicJSON (adds `cogeo-mosaic` requirement)
 [**titiler.application**](https://github.com/developmentseed/titiler/tree/main/src/titiler/application) | [![titiler.application](https://img.shields.io/pypi/v/titiler.application?color=%2334D058&label=pypi)](https://pypi.org/project/titiler.application) | TiTiler's `demo` package. Contains a FastAPI application with full support of COG, STAC and MosaicJSON
@@ -71,6 +73,7 @@ python -m pip install -U pip
 python -m pip  install titiler.{package}
 # e.g.,
 # python -m pip  install titiler.core
+# python -m pip  install titiler.xarray
 # python -m pip  install titiler.extensions
 # python -m pip  install titiler.mosaic
 # python -m pip  install titiler.application (also installs core, extensions and mosaic)
@@ -89,7 +92,7 @@ git clone https://github.com/developmentseed/titiler.git
 cd titiler
 
 python -m pip install -U pip
-python -m pip install -e src/titiler/core -e src/titiler/extensions -e src/titiler/mosaic -e src/titiler/application
+python -m pip install -e src/titiler/core -e src/titiler/xarray -e src/titiler/extensions -e src/titiler/mosaic -e src/titiler/application
 python -m pip install uvicorn
 
 uvicorn titiler.application.main:app --reload
@@ -102,11 +105,11 @@ Ready to use/deploy images can be found on Github registry.
 - https://github.com/developmentseed/titiler/pkgs/container/titiler
 
 ```bash
-docker run --name titiler \
+docker run \
+    --platform=linux/amd64 \
     -p 8000:8000 \
-    --env PORT=8000 \
-    --env WORKERS_PER_CORE=1 \
-    --rm -it ghcr.io/developmentseed/titiler:latest
+    --rm -it ghcr.io/developmentseed/titiler:latest \
+    uvicorn titiler.application.main:app --host 0.0.0.0 --port 8000 --workers 1
 ```
 
 - Built the docker locally
@@ -114,17 +117,16 @@ docker run --name titiler \
 git clone https://github.com/developmentseed/titiler.git
 cd titiler
 
-docker-compose up --build titiler  # or titiler-uvicorn
+docker compose up --build titiler
 ```
 
-Some options can be set via environment variables, see: https://github.com/tiangolo/uvicorn-gunicorn-docker#advanced-usage
-
 ## Project structure
 
 ```
 src/titiler/                     - titiler modules.
  ├── application/                - Titiler's `Application` package
  ├── extensions/                 - Titiler's `Extensions` package
+ ├── xarray/                     - Titiler's `Xarray` package
  ├── core/                       - Titiler's `Core` package
  └── mosaic/                     - Titiler's `Mosaic` package
 ```
diff --git a/RELEASING.md b/RELEASING.md
new file mode 100644
index 000000000..cc18b39c6
--- /dev/null
+++ b/RELEASING.md
@@ -0,0 +1,22 @@
+# Releasing
+
+This is a checklist for releasing a new version of **titiler**.
+
+1. Determine the next version
+   We currently do not have published versioning guidelines. We usually use `minor` version update when pushing breaking changes and `patch` for every other updates
+2. Create a release branch named `release/vX.Y.Z`, where `X.Y.Z` is the new version
+3. Search and replace all instances of the current version number with the new version
+
+   We recommend to use [`bump-my-version`](https://github.com/callowayproject/bump-my-version) CLI
+   ```
+   bump-my-version bump --new-version 3.1.0
+   ```
+
+4. Manually increase the helm chart `version` in `/deployment/k8s/charts/Chart.yaml (not matching titiler's version)
+5. Update [CHANGES.md](./CHANGES.md) for the new version
+6. Push your release branch, create a PR, and get approval
+7. Once the PR is merged, create a new (annotated, signed) tag on the appropriate commit
+   Name the tag `X.Y.Z`, and include `vX.Y.Z` as its annotation message
+8. Push your tag to Github, which will kick off the publishing workflow
+9. Create a [new release](https://github.com/stac-utils/stac-fastapi/releases/new) targeting the new tag, and use the "Generate release notes" feature to populate the description.
+    Publish the release and mark it as the latest.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 000000000..d353fc5bd
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,73 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+If there are any vulnerabilities in `titiler`, don't hesitate to _report them_.
+
+1. Use Github's security reporting tools.
+
+see https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability
+
+2. Describe the vulnerability.
+
+   If you have a fix, that is most welcome -- please attach or summarize it in your message!
+
+3. We will evaluate the vulnerability and, if necessary, release a fix or mitigating steps to address it. We will contact you to let you know the outcome, and will credit you in the report.
+
+   Please **do not disclose the vulnerability publicly** until a fix is released!
+
+4. Once we have either a) published a fix, or b) declined to address the vulnerability for whatever reason, you are free to publicly disclose it.
+
+
+## GDAL
+
+`TiTiler` is built on top of Rasterio which is a python wrapper for the [GDAL](https://gdal.org/en/stable/) C++ library. At the time of writing, GDAL is responsible for most of the I/O and thus is where vulnerabilities could be harmful. For any `I/O` issues please first check [GDAL documentation](https://gdal.org/en/stable/user/security.html#security-considerations).
+
+#### GDAL VRT Driver
+
+There is a known security vulnerability with the VRT Driver:
+
+> It can be used to access any valid GDAL dataset. If a hostile party, with knowledge of the location on the filesystem of a valid GDAL dataset, convinces a user to run gdal_translate a VRT file and give it back the result, it might be able to steal data. That could potentially be able for a web service accepting data from the user, converting it, and sending back the result.
+
+see https://gdal.org/en/stable/user/security.html#gdal-vrt-driver
+
+Thus we recommend deploying titiler in infrastructure with limited access to the filesystem. Users can also `disable` the VRT driver completely by using `GDAL_SKIP=VRT` environment variable.
+
+In GDAL 3.12, new environment variables might be introduced to enable more control over the VRT driver: https://github.com/OSGeo/gdal/pull/12669
+
+#### Limit source's host
+
+If users want to limit the sources that the application can access, they can also create custom `path_dependency` such as this one which limits valid sources to a list of known hosts:
+
+```python
+from urllib.parse import urlparse
+
+from typing import Annotated
+from titiler.core.factory import TilerFactory
+from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers
+
+from fastapi import FastAPI, Query, HTTPException
+
+# List of known host where dataset can be read from
+known_host = [
+   "devseed.org",
+]
+
+def DatasetPathParams(url: Annotated[str, Query(description="Dataset URL")]) -> str:
+   """Create dataset path from args"""
+   # validate Dataset host
+   parsed = urlparse(url)
+   if parsed.netloc not in known_host:
+      raise HTTPException(
+         status_code=400,
+         detail="Nope, this is not a valid File - Please Try Again",
+      )
+
+   return url
+
+
+app = FastAPI(title="My simple app")
+app.include_router(TilerFactory(path_dependency=DatasetPathParams).router)
+
+add_exception_handlers(app, DEFAULT_STATUS_CODES)
+```
diff --git a/deployment/aws/README.md b/deployment/aws/README.md
index de6c243f9..742810b87 100644
--- a/deployment/aws/README.md
+++ b/deployment/aws/README.md
@@ -4,4 +4,3 @@ Intro: https://developmentseed.org/titiler/deployment/aws/intro/
 
 AWS lambda: https://developmentseed.org/titiler/deployment/aws/lambda/
 
-ECS/Fargate: https://developmentseed.org/titiler/deployment/aws/ecs/
diff --git a/deployment/aws/cdk/app.py b/deployment/aws/cdk/app.py
index 235baa35f..d7d155e4d 100644
--- a/deployment/aws/cdk/app.py
+++ b/deployment/aws/cdk/app.py
@@ -1,17 +1,14 @@
 """Construct App."""
 
 import os
-from typing import Any, Dict, List, Optional, Union
+from typing import Any, Dict, List, Optional
 
 from aws_cdk import App, CfnOutput, Duration, Stack, Tags
-from aws_cdk import aws_apigatewayv2_alpha as apigw
-from aws_cdk import aws_ec2 as ec2
-from aws_cdk import aws_ecs as ecs
-from aws_cdk import aws_ecs_patterns as ecs_patterns
+from aws_cdk import aws_apigatewayv2 as apigw
 from aws_cdk import aws_iam as iam
 from aws_cdk import aws_lambda
 from aws_cdk import aws_logs as logs
-from aws_cdk.aws_apigatewayv2_integrations_alpha import HttpLambdaIntegration
+from aws_cdk.aws_apigatewayv2_integrations import HttpLambdaIntegration
 from config import StackSettings
 from constructs import Construct
 
@@ -34,7 +31,7 @@ def __init__(
         id: str,
         memory: int = 1024,
         timeout: int = 30,
-        runtime: aws_lambda.Runtime = aws_lambda.Runtime.PYTHON_3_10,
+        runtime: aws_lambda.Runtime = aws_lambda.Runtime.PYTHON_3_12,
         concurrent: Optional[int] = None,
         permissions: Optional[List[iam.PolicyStatement]] = None,
         environment: Optional[Dict] = None,
@@ -47,6 +44,7 @@ def __init__(
         permissions = permissions or []
         environment = environment or {}
 
+        # COG / STAC / MosaicJSON
         lambda_function = aws_lambda.Function(
             self,
             f"{id}-lambda",
@@ -54,6 +52,10 @@ def __init__(
             code=aws_lambda.Code.from_docker_build(
                 path=os.path.abspath(code_dir),
                 file="lambda/Dockerfile",
+                platform="linux/amd64",
+                build_args={
+                    "PYTHON_VERSION": "3.12",
+                },
             ),
             handler="handler.handler",
             memory_size=memory,
@@ -75,91 +77,38 @@ def __init__(
         )
         CfnOutput(self, "Endpoint", value=api.url)
 
-
-class titilerECSStack(Stack):
-    """Titiler ECS Fargate Stack."""
-
-    def __init__(
-        self,
-        scope: Construct,
-        id: str,
-        cpu: Union[int, float] = 256,
-        memory: Union[int, float] = 512,
-        mincount: int = 1,
-        maxcount: int = 50,
-        permissions: Optional[List[iam.PolicyStatement]] = None,
-        environment: Optional[Dict] = None,
-        code_dir: str = "./",
-        **kwargs: Any,
-    ) -> None:
-        """Define stack."""
-        super().__init__(scope, id, *kwargs)
-
-        permissions = permissions or []
-        environment = environment or {}
-
-        vpc = ec2.Vpc(self, f"{id}-vpc", max_azs=2)
-
-        cluster = ecs.Cluster(self, f"{id}-cluster", vpc=vpc)
-
-        task_env = environment.copy()
-        task_env.update({"LOG_LEVEL": "error"})
-
-        # GUNICORN configuration
-        if settings.workers_per_core:
-            task_env.update({"WORKERS_PER_CORE": str(settings.workers_per_core)})
-        if settings.max_workers:
-            task_env.update({"MAX_WORKERS": str(settings.max_workers)})
-        if settings.web_concurrency:
-            task_env.update({"WEB_CONCURRENCY": str(settings.web_concurrency)})
-
-        fargate_service = ecs_patterns.ApplicationLoadBalancedFargateService(
+        # Xarray
+        xarray_lambda_function = aws_lambda.Function(
             self,
-            f"{id}-service",
-            cluster=cluster,
-            cpu=cpu,
-            memory_limit_mib=memory,
-            desired_count=mincount,
-            public_load_balancer=True,
-            listener_port=80,
-            task_image_options=ecs_patterns.ApplicationLoadBalancedTaskImageOptions(
-                image=ecs.ContainerImage.from_registry(
-                    f"ghcr.io/developmentseed/titiler:{settings.image_version}",
-                ),
-                container_port=80,
-                environment=task_env,
+            f"{id}-xarray-lambda",
+            runtime=runtime,
+            code=aws_lambda.Code.from_docker_build(
+                path=os.path.abspath(code_dir),
+                file="lambda/Dockerfile.xarray",
+                platform="linux/amd64",
+                build_args={
+                    "PYTHON_VERSION": "3.12",
+                },
             ),
+            handler="handler.handler",
+            memory_size=memory,
+            reserved_concurrent_executions=concurrent,
+            timeout=Duration.seconds(timeout),
+            environment=environment,
+            log_retention=logs.RetentionDays.ONE_WEEK,
         )
-        fargate_service.target_group.configure_health_check(path="/healthz")
 
         for perm in permissions:
-            fargate_service.task_definition.task_role.add_to_policy(perm)
+            xarray_lambda_function.add_to_role_policy(perm)
 
-        scalable_target = fargate_service.service.auto_scale_task_count(
-            min_capacity=mincount, max_capacity=maxcount
-        )
-
-        # https://github.com/awslabs/aws-rails-provisioner/blob/263782a4250ca1820082bfb059b163a0f2130d02/lib/aws-rails-provisioner/scaling.rb#L343-L387
-        scalable_target.scale_on_request_count(
-            "RequestScaling",
-            requests_per_target=50,
-            scale_in_cooldown=Duration.seconds(240),
-            scale_out_cooldown=Duration.seconds(30),
-            target_group=fargate_service.target_group,
-        )
-
-        # scalable_target.scale_on_cpu_utilization(
-        #     "CpuScaling", target_utilization_percent=70,
-        # )
-
-        fargate_service.service.connections.allow_from_any_ipv4(
-            port_range=ec2.Port(
-                protocol=ec2.Protocol.ALL,
-                string_representation="All port 80",
-                from_port=80,
+        xarray_api = apigw.HttpApi(
+            self,
+            f"{id}-xarray-endpoint",
+            default_integration=HttpLambdaIntegration(
+                f"{id}-xarray-integration", handler=xarray_lambda_function
             ),
-            description="Allows traffic on port 80 from ALB",
         )
+        CfnOutput(self, "Xarray-Endpoint", value=xarray_api.url)
 
 
 app = App()
@@ -175,18 +124,6 @@ def __init__(
         )
     )
 
-
-ecs_stack = titilerECSStack(
-    app,
-    f"{settings.name}-ecs-{settings.stage}",
-    cpu=settings.task_cpu,
-    memory=settings.task_memory,
-    mincount=settings.min_ecs_instances,
-    maxcount=settings.max_ecs_instances,
-    permissions=perms,
-    environment=settings.env,
-)
-
 lambda_stack = titilerLambdaStack(
     app,
     f"{settings.name}-lambda-{settings.stage}",
@@ -205,7 +142,6 @@ def __init__(
     "Client": settings.client,
 }.items():
     if value:
-        Tags.of(ecs_stack).add(key, value)
         Tags.of(lambda_stack).add(key, value)
 
 
diff --git a/deployment/aws/cdk/config.py b/deployment/aws/cdk/config.py
index fa016b193..bebf52d62 100644
--- a/deployment/aws/cdk/config.py
+++ b/deployment/aws/cdk/config.py
@@ -2,17 +2,17 @@
 
 from typing import Dict, List, Optional
 
-import pydantic
+from pydantic_settings import BaseSettings, SettingsConfigDict
 
 
-class StackSettings(pydantic.BaseSettings):
+class StackSettings(BaseSettings):
     """Application settings"""
 
     name: str = "titiler"
     stage: str = "production"
 
-    owner: Optional[str]
-    client: Optional[str]
+    owner: Optional[str] = None
+    client: Optional[str] = None
 
     # Default options are optimized for CloudOptimized GeoTIFF
     # For more information on GDAL env see: https://gdal.org/user/configoptions.html
@@ -38,42 +38,6 @@ class StackSettings(pydantic.BaseSettings):
     # S3 key pattern to limit the access to specific items (e.g: "my_data/*.tif")
     key: str = "*"
 
-    ###########################################################################
-    # AWS ECS
-    # The following settings only apply to AWS ECS deployment
-    min_ecs_instances: int = 5
-    max_ecs_instances: int = 50
-
-    # CPU value      |   Memory value
-    # 256 (.25 vCPU) | 0.5 GB, 1 GB, 2 GB
-    # 512 (.5 vCPU)  | 1 GB, 2 GB, 3 GB, 4 GB
-    # 1024 (1 vCPU)  | 2 GB, 3 GB, 4 GB, 5 GB, 6 GB, 7 GB, 8 GB
-    # 2048 (2 vCPU)  | Between 4 GB and 16 GB in 1-GB increments
-    # 4096 (4 vCPU)  | Between 8 GB and 30 GB in 1-GB increments
-    task_cpu: int = 256
-    task_memory: int = 512
-
-    # GUNICORN configuration
-    # Ref: https://github.com/developmentseed/titiler/issues/119
-
-    # WORKERS_PER_CORE
-    # This image will check how many CPU cores are available in the current server running your container.
-    # It will set the number of workers to the number of CPU cores multiplied by this value.
-    workers_per_core: int = 1
-
-    # MAX_WORKERS
-    # You can use it to let the image compute the number of workers automatically but making sure it's limited to a maximum.
-    # should depends on `task_cpu`
-    max_workers: int = 1
-
-    # WEB_CONCURRENCY
-    # Override the automatic definition of number of workers.
-    # Set to the number of CPU cores in the current server multiplied by the environment variable WORKERS_PER_CORE.
-    # So, in a server with 2 cores, by default it will be set to 2.
-    web_concurrency: Optional[int]
-
-    image_version: str = "latest"
-
     ###########################################################################
     # AWS LAMBDA
     # The following settings only apply to AWS Lambda deployment
@@ -83,10 +47,6 @@ class StackSettings(pydantic.BaseSettings):
 
     # The maximum of concurrent executions you want to reserve for the function.
     # Default: - No specific limit - account limit.
-    max_concurrent: Optional[int]
-
-    class Config:
-        """model config"""
+    max_concurrent: Optional[int] = None
 
-        env_file = ".env"
-        env_prefix = "TITILER_STACK_"
+    model_config = SettingsConfigDict(env_prefix="TITILER_STACK_", env_file=".env")
diff --git a/deployment/aws/lambda/Dockerfile b/deployment/aws/lambda/Dockerfile
index 9e8fc276e..ad9b8be95 100644
--- a/deployment/aws/lambda/Dockerfile
+++ b/deployment/aws/lambda/Dockerfile
@@ -1,11 +1,14 @@
-ARG PYTHON_VERSION=3.10
+ARG PYTHON_VERSION=3.12
 
 FROM --platform=linux/amd64 public.ecr.aws/lambda/python:${PYTHON_VERSION}
 
 WORKDIR /tmp
 
-RUN pip install pip -U
-RUN pip install "titiler.application==0.11.7" "mangum>=0.10.0" -t /asset --no-binary pydantic
+# Install system dependencies to compile (numexpr)
+RUN dnf install -y gcc-c++ && dnf clean all
+
+RUN python -m pip install pip -U
+RUN python -m pip install "titiler.application==0.22.4" "mangum>=0.10.0" -t /asset --no-binary pydantic
 
 # Reduce package size and remove useless files
 RUN cd /asset && find . -type f -name '*.pyc' | while read f; do n=$(echo $f | sed 's/__pycache__\///' | sed 's/.cpython-[0-9]*//'); cp $f $n; done;
@@ -16,4 +19,10 @@ RUN rm -rdf /asset/numpy/doc/ /asset/boto3* /asset/botocore* /asset/bin /asset/g
 
 COPY lambda/handler.py /asset/handler.py
 
+# Ref: https://github.com/developmentseed/titiler/discussions/1108#discussioncomment-13045681
+RUN cp /usr/lib64/libexpat.so.1 /asset/
+
+WORKDIR /asset
+RUN python -c "from handler import handler; print('All Good')"
+
 CMD ["echo", "hello world"]
diff --git a/deployment/aws/lambda/Dockerfile.xarray b/deployment/aws/lambda/Dockerfile.xarray
new file mode 100644
index 000000000..c95e1a968
--- /dev/null
+++ b/deployment/aws/lambda/Dockerfile.xarray
@@ -0,0 +1,28 @@
+ARG PYTHON_VERSION=3.12
+
+FROM --platform=linux/amd64 public.ecr.aws/lambda/python:${PYTHON_VERSION}
+
+WORKDIR /tmp
+
+# Install system dependencies to compile (numexpr)
+RUN dnf install -y gcc-c++ && dnf clean all
+
+RUN python -m pip install pip -U
+RUN python -m pip install "titiler.xarray==0.22.4" "mangum>=0.10.0" "aiobotocore==2.17.0" "zarr" "s3fs" "aiohttp" "h5netcdf" "starlette-cramjam" "cftime" -t /asset --no-binary pydantic,xarray,numpy,pandas
+
+# Reduce package size and remove useless files
+RUN cd /asset && find . -type f -name '*.pyc' | while read f; do n=$(echo $f | sed 's/__pycache__\///' | sed 's/.cpython-[0-9]*//'); cp $f $n; done;
+RUN cd /asset && find . -type d -a -name '__pycache__' -print0 | xargs -0 rm -rf
+RUN cd /asset && find . -type f -a -name '*.py' -print0 | xargs -0 rm -f
+RUN find /asset -type d -a -name 'tests' -print0 | xargs -0 rm -rf
+RUN rm -rdf /asset/numpy/doc/ /asset/boto3* /asset/botocore* /asset/bin /asset/geos_license /asset/Misc
+
+COPY lambda/xarray_handler.py /asset/handler.py
+
+# Ref: https://github.com/developmentseed/titiler/discussions/1108#discussioncomment-13045681
+RUN cp /usr/lib64/libexpat.so.1 /asset/
+
+WORKDIR /asset
+RUN python -c "from handler import handler; print('All Good')"
+
+CMD ["echo", "hello world"]
diff --git a/deployment/aws/lambda/handler.py b/deployment/aws/lambda/handler.py
index 4e66c7b2e..933807be5 100644
--- a/deployment/aws/lambda/handler.py
+++ b/deployment/aws/lambda/handler.py
@@ -5,8 +5,11 @@
 from mangum import Mangum
 
 from titiler.application.main import app
+from titiler.application.settings import ApiSettings
 
 logging.getLogger("mangum.lifespan").setLevel(logging.ERROR)
 logging.getLogger("mangum.http").setLevel(logging.ERROR)
 
-handler = Mangum(app, lifespan="auto")
+api_settings = ApiSettings()
+
+handler = Mangum(app, api_gateway_base_path=api_settings.root_path, lifespan="auto")
diff --git a/deployment/aws/lambda/xarray_handler.py b/deployment/aws/lambda/xarray_handler.py
new file mode 100644
index 000000000..81c84d4b9
--- /dev/null
+++ b/deployment/aws/lambda/xarray_handler.py
@@ -0,0 +1,281 @@
+"""AWS Lambda handler."""
+
+import logging
+from typing import Annotated, Literal, Optional
+
+import rasterio
+import xarray
+import zarr
+from fastapi import FastAPI, Query
+from mangum import Mangum
+from starlette.middleware.cors import CORSMiddleware
+from starlette.requests import Request
+from starlette_cramjam.middleware import CompressionMiddleware
+
+from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers
+from titiler.core.factory import AlgorithmFactory, ColorMapFactory, TMSFactory
+from titiler.core.middleware import CacheControlMiddleware
+from titiler.core.models.OGC import Conformance, Landing
+from titiler.core.resources.enums import MediaType
+from titiler.core.templating import create_html_response
+from titiler.core.utils import accept_media_type, update_openapi
+from titiler.xarray import __version__ as titiler_version
+from titiler.xarray.extensions import DatasetMetadataExtension
+from titiler.xarray.factory import TilerFactory
+
+logging.getLogger("mangum.lifespan").setLevel(logging.ERROR)
+logging.getLogger("mangum.http").setLevel(logging.ERROR)
+
+app = FastAPI(
+    title="TiTiler with support of Multidimensional dataset",
+    description="""A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL/Xarray for Zarr/NetCDF dataset.
+
+---
+
+**Documentation**: https://developmentseed.org/titiler/
+
+**Source Code**: https://github.com/developmentseed/titiler
+
+---
+    """,
+    openapi_url="/api",
+    docs_url="/api.html",
+    version=titiler_version,
+)
+
+update_openapi(app)
+
+TITILER_CONFORMS_TO = {
+    "http://www.opengis.net/spec/ogcapi-common-1/1.0/req/core",
+    "http://www.opengis.net/spec/ogcapi-common-1/1.0/req/landing-page",
+    "http://www.opengis.net/spec/ogcapi-common-1/1.0/req/oas30",
+    "http://www.opengis.net/spec/ogcapi-common-1/1.0/req/html",
+    "http://www.opengis.net/spec/ogcapi-common-1/1.0/req/json",
+}
+
+
+md = TilerFactory(
+    extensions=[
+        DatasetMetadataExtension(),
+    ],
+)
+app.include_router(md.router, tags=["Multi Dimensional"])
+
+TITILER_CONFORMS_TO.update(md.conforms_to)
+
+# TileMatrixSets endpoints
+tms = TMSFactory()
+app.include_router(tms.router, tags=["Tiling Schemes"])
+TITILER_CONFORMS_TO.update(tms.conforms_to)
+
+###############################################################################
+# Algorithms endpoints
+algorithms = AlgorithmFactory()
+app.include_router(
+    algorithms.router,
+    tags=["Algorithms"],
+)
+TITILER_CONFORMS_TO.update(algorithms.conforms_to)
+
+# Colormaps endpoints
+cmaps = ColorMapFactory()
+app.include_router(
+    cmaps.router,
+    tags=["ColorMaps"],
+)
+TITILER_CONFORMS_TO.update(cmaps.conforms_to)
+
+add_exception_handlers(app, DEFAULT_STATUS_CODES)
+
+# Set all CORS enabled origins
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins="*",
+    allow_credentials=True,
+    allow_methods=["GET"],
+    allow_headers=["*"],
+)
+
+app.add_middleware(
+    CompressionMiddleware,
+    minimum_size=0,
+    exclude_mediatype={
+        "image/jpeg",
+        "image/jpg",
+        "image/png",
+        "image/jp2",
+        "image/webp",
+    },
+    compression_level=6,
+)
+
+app.add_middleware(
+    CacheControlMiddleware,
+    cachecontrol="public, max-age=3600",
+    exclude_path={r"/healthz"},
+)
+
+
+@app.get(
+    "/healthz",
+    description="Health Check.",
+    summary="Health Check.",
+    operation_id="healthCheck",
+    tags=["Health Check"],
+)
+def application_health_check():
+    """Health check."""
+    return {
+        "versions": {
+            "titiler": titiler_version,
+            "rasterio": rasterio.__version__,
+            "gdal": rasterio.__gdal_version__,
+            "proj": rasterio.__proj_version__,
+            "geos": rasterio.__geos_version__,
+            "xarray": xarray.__version__,
+            "zarr": zarr.__version__,
+        }
+    }
+
+
+@app.get(
+    "/",
+    response_model=Landing,
+    response_model_exclude_none=True,
+    responses={
+        200: {
+            "content": {
+                "text/html": {},
+                "application/json": {},
+            }
+        },
+    },
+    tags=["OGC Common"],
+)
+def landing(
+    request: Request,
+    f: Annotated[
+        Optional[Literal["html", "json"]],
+        Query(
+            description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header."
+        ),
+    ] = None,
+):
+    """TiTiler landing page."""
+    data = {
+        "title": "TiTiler + Xarray",
+        "description": "A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL/Xarray for Zarr/NetCDF dataset.",
+        "links": [
+            {
+                "title": "Landing page",
+                "href": str(request.url_for("landing")),
+                "type": "text/html",
+                "rel": "self",
+            },
+            {
+                "title": "The API definition (JSON)",
+                "href": str(request.url_for("openapi")),
+                "type": "application/vnd.oai.openapi+json;version=3.0",
+                "rel": "service-desc",
+            },
+            {
+                "title": "The API documentation",
+                "href": str(request.url_for("swagger_ui_html")),
+                "type": "text/html",
+                "rel": "service-doc",
+            },
+            {
+                "title": "Conformance Declaration",
+                "href": str(request.url_for("conformance")),
+                "type": "text/html",
+                "rel": "http://www.opengis.net/def/rel/ogc/1.0/conformance",
+            },
+            {
+                "title": "TiTiler Documentation (external link)",
+                "href": "https://developmentseed.org/titiler/",
+                "type": "text/html",
+                "rel": "doc",
+            },
+            {
+                "title": "TiTiler.Xarray source code (external link)",
+                "href": "https://github.com/developmentseed/titiler/tree/main/src/titiler/xarray",
+                "type": "text/html",
+                "rel": "doc",
+            },
+        ],
+    }
+
+    output_type: Optional[MediaType]
+    if f:
+        output_type = MediaType[f]
+    else:
+        accepted_media = [MediaType.html, MediaType.json]
+        output_type = accept_media_type(
+            request.headers.get("accept", ""), accepted_media
+        )
+
+    if output_type == MediaType.html:
+        return create_html_response(
+            request,
+            data,
+            title="TiTiler",
+            template_name="landing",
+        )
+
+    return data
+
+
+@app.get(
+    "/conformance",
+    response_model=Conformance,
+    response_model_exclude_none=True,
+    responses={
+        200: {
+            "content": {
+                "text/html": {},
+                "application/json": {},
+            }
+        },
+    },
+    tags=["OGC Common"],
+)
+def conformance(
+    request: Request,
+    f: Annotated[
+        Optional[Literal["html", "json"]],
+        Query(
+            description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header."
+        ),
+    ] = None,
+):
+    """Conformance classes.
+
+    Called with `GET /conformance`.
+
+    Returns:
+        Conformance classes which the server conforms to.
+
+    """
+    data = {"conformsTo": sorted(TITILER_CONFORMS_TO)}
+
+    output_type: Optional[MediaType]
+    if f:
+        output_type = MediaType[f]
+    else:
+        accepted_media = [MediaType.html, MediaType.json]
+        output_type = accept_media_type(
+            request.headers.get("accept", ""), accepted_media
+        )
+
+    if output_type == MediaType.html:
+        return create_html_response(
+            request,
+            data,
+            title="Conformance",
+            template_name="conformance",
+        )
+
+    return data
+
+
+handler = Mangum(app, lifespan="auto")
diff --git a/deployment/aws/package-lock.json b/deployment/aws/package-lock.json
index ad5226462..eec48cd4d 100644
--- a/deployment/aws/package-lock.json
+++ b/deployment/aws/package-lock.json
@@ -9,35 +9,35 @@
       "version": "0.1.0",
       "license": "MIT",
       "dependencies": {
-        "cdk": "2.76.0-alpha.0"
+        "cdk": "2.1018.1"
       }
     },
     "node_modules/aws-cdk": {
-      "version": "2.76.0",
-      "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.76.0.tgz",
-      "integrity": "sha512-y6VHtqUpYenn6mGIBFbcGGXIoXfKA3o0eGL/eeD/gUJ9TcPrgMLQM1NxSMb5JVsOk5BPPXzGmvB0gBu40utGqg==",
+      "version": "2.1018.1",
+      "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1018.1.tgz",
+      "integrity": "sha512-kFPRox5kSm+ktJ451o0ng9rD+60p5Kt1CZIWw8kXnvqbsxN2xv6qbmyWSXw7sGVXVwqrRKVj+71/JeDr+LMAZw==",
       "bin": {
         "cdk": "bin/cdk"
       },
       "engines": {
-        "node": ">= 14.15.0"
+        "node": ">= 18.0.0"
       },
       "optionalDependencies": {
         "fsevents": "2.3.2"
       }
     },
     "node_modules/cdk": {
-      "version": "2.76.0-alpha.0",
-      "resolved": "https://registry.npmjs.org/cdk/-/cdk-2.76.0-alpha.0.tgz",
-      "integrity": "sha512-HNfX5c7MU18LxthZRcapqEhG0IFgQeNOhtsTR1QiL/7dhy2TjvK26dYcJ67KIHfzMfm5EUjvOXdP1SPdW+eOOA==",
+      "version": "2.1018.1",
+      "resolved": "https://registry.npmjs.org/cdk/-/cdk-2.1018.1.tgz",
+      "integrity": "sha512-lm4dZJHZqi8jdNMmVptllWpvieGJgjjbrBZDKBsx6Okqlj3MoYj7OK6zpoKz7HQ9hI7SULw6TXRtxsSSx1UByA==",
       "dependencies": {
-        "aws-cdk": "2.76.0"
+        "aws-cdk": "2.1018.1"
       },
       "bin": {
         "cdk": "bin/cdk"
       },
       "engines": {
-        "node": ">= 8.10.0"
+        "node": ">= 18.0.0"
       }
     },
     "node_modules/fsevents": {
@@ -56,19 +56,19 @@
   },
   "dependencies": {
     "aws-cdk": {
-      "version": "2.76.0",
-      "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.76.0.tgz",
-      "integrity": "sha512-y6VHtqUpYenn6mGIBFbcGGXIoXfKA3o0eGL/eeD/gUJ9TcPrgMLQM1NxSMb5JVsOk5BPPXzGmvB0gBu40utGqg==",
+      "version": "2.1018.1",
+      "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1018.1.tgz",
+      "integrity": "sha512-kFPRox5kSm+ktJ451o0ng9rD+60p5Kt1CZIWw8kXnvqbsxN2xv6qbmyWSXw7sGVXVwqrRKVj+71/JeDr+LMAZw==",
       "requires": {
         "fsevents": "2.3.2"
       }
     },
     "cdk": {
-      "version": "2.76.0-alpha.0",
-      "resolved": "https://registry.npmjs.org/cdk/-/cdk-2.76.0-alpha.0.tgz",
-      "integrity": "sha512-HNfX5c7MU18LxthZRcapqEhG0IFgQeNOhtsTR1QiL/7dhy2TjvK26dYcJ67KIHfzMfm5EUjvOXdP1SPdW+eOOA==",
+      "version": "2.1018.1",
+      "resolved": "https://registry.npmjs.org/cdk/-/cdk-2.1018.1.tgz",
+      "integrity": "sha512-lm4dZJHZqi8jdNMmVptllWpvieGJgjjbrBZDKBsx6Okqlj3MoYj7OK6zpoKz7HQ9hI7SULw6TXRtxsSSx1UByA==",
       "requires": {
-        "aws-cdk": "2.76.0"
+        "aws-cdk": "2.1018.1"
       }
     },
     "fsevents": {
diff --git a/deployment/aws/package.json b/deployment/aws/package.json
index 040bfa6b4..f0a4fec63 100644
--- a/deployment/aws/package.json
+++ b/deployment/aws/package.json
@@ -5,7 +5,7 @@
   "license": "MIT",
   "private": true,
   "dependencies": {
-    "cdk": "2.76.0-alpha.0"
+    "cdk": "2.1018.1"
   },
   "scripts": {
     "cdk": "cdk"
diff --git a/deployment/aws/requirements-cdk.txt b/deployment/aws/requirements-cdk.txt
index 973fc3433..5e33bf5f4 100644
--- a/deployment/aws/requirements-cdk.txt
+++ b/deployment/aws/requirements-cdk.txt
@@ -1,9 +1,7 @@
 # aws cdk
-aws-cdk-lib==2.76.0
-aws_cdk-aws_apigatewayv2_alpha==2.76.0a0
-aws_cdk-aws_apigatewayv2_integrations_alpha==2.76.0a0
+aws-cdk-lib==2.201.0
 constructs>=10.0.0
 
 # pydantic settings
-pydantic
-python-dotenv
+pydantic~=2.0
+pydantic-settings~=2.0
diff --git a/deployment/azure/README.md b/deployment/azure/README.md
index 7516cde29..7833f50b0 100644
--- a/deployment/azure/README.md
+++ b/deployment/azure/README.md
@@ -1,13 +1,13 @@
 ### Function
 
-TiTiler is built on top of [FastAPI](https://github.com/tiangolo/fastapi), a modern, fast, Python web framework for building APIs. As for AWS Lambda we can make our FastAPI application work on Azure Function by wrapping it within the [Azure Function Python worker](https://github.com/Azure/azure-functions-python-worker).
+TiTiler is built on top of [FastAPI](https://github.com/tiangolo/fastapi), a modern, fast, Python web framework for building APIs. We can make our FastAPI application work as an Azure Function by wrapping it within the [Azure Function Python worker](https://github.com/Azure/azure-functions-python-worker).
 
 If you are not familiar with **Azure functions** we recommend checking https://docs.microsoft.com/en-us/azure/azure-functions/ first.
 
 Minimal TiTiler Azure function code:
 ```python
 import azure.functions as func
-from titiler.application.routers import cog, mosaic, stac, tms
+from titiler.application.main import cog, mosaic, stac, tms
 from fastapi import FastAPI
 
 
@@ -20,14 +20,12 @@ app.include_router(mosaic.router, prefix="/mosaicjson", tags=["MosaicJSON"])
 app.include_router(tms.router, tags=["TileMatrixSets"])
 
 
-def main(
+async def main(
     req: func.HttpRequest, context: func.Context,
 ) -> func.HttpResponse:
-    return func.AsgiMiddleware(app).handle(req, context)
+    return await func.AsgiMiddleware(app).handle_async(req, context)
 ```
 
-Note: there is a `bug` in `azure.functions.AsgiMiddleware` which prevent using `starlette.BaseHTTPMiddleware` middlewares (see: https://github.com/Azure/azure-functions-python-worker/issues/903).
-
 #### Requirements
 - Azure CLI: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli
 - Azure Function Tool: https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local
@@ -42,9 +40,9 @@ $ cd titiler/deployment/azure
 
 $ az login
 $ az group create --name AzureFunctionsTiTiler-rg --location eastus
-$ az storage account create --name TiTilerStorage --sku Standard_LRS
-$ az functionapp create --consumption-plan-location eastus --runtime python --runtime-version 3.8 --functions-version 3 --name titiler --os-type linux
-$ func azure functionapp publish titiler
+$ az storage account create --name {your-new-storage-name} --sku Standard_LRS -g AzureFunctionsTiTiler-rg
+$ az functionapp create --consumption-plan-location eastus --runtime python --runtime-version 3.9 --functions-version 4 --name {your-new-function-name} --os-type linux -g AzureFunctionsTiTiler-rg -s {your-new-storage-name}
+$ func azure functionapp publish titiler --python
 ```
 
 or
diff --git a/deployment/azure/app/__init__.py b/deployment/azure/app/__init__.py
index 5aab2727d..33e1d2939 100644
--- a/deployment/azure/app/__init__.py
+++ b/deployment/azure/app/__init__.py
@@ -8,17 +8,15 @@
 from starlette_cramjam.middleware import CompressionMiddleware
 
 from titiler.application import __version__ as titiler_version
-from titiler.application.custom import templates
-from titiler.application.routers import cog, mosaic, stac, tms
+from titiler.application.main import cog, mosaic, stac, templates, tms
 from titiler.application.settings import ApiSettings
 from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers
-
-# from titiler.core.middleware import (
-#     CacheControlMiddleware,
-#     LoggerMiddleware,
-#     LowerCaseQueryStringMiddleware,
-#     TotalTimeMiddleware,
-# )
+from titiler.core.middleware import (
+    CacheControlMiddleware,
+    LoggerMiddleware,
+    LowerCaseQueryStringMiddleware,
+    TotalTimeMiddleware,
+)
 from titiler.mosaic.errors import MOSAIC_STATUS_CODES
 
 api_settings = ApiSettings()
@@ -68,19 +66,18 @@
     },
 )
 
-# see https://github.com/encode/starlette/issues/1320
-# app.add_middleware(
-#     CacheControlMiddleware,
-#     cachecontrol=api_settings.cachecontrol,
-#     exclude_path={r"/healthz"},
-# )
+app.add_middleware(
+    CacheControlMiddleware,
+    cachecontrol=api_settings.cachecontrol,
+    exclude_path={r"/healthz"},
+)
 
-# if api_settings.debug:
-#     app.add_middleware(LoggerMiddleware, headers=True, querystrings=True)
-#     app.add_middleware(TotalTimeMiddleware)
+if api_settings.debug:
+    app.add_middleware(LoggerMiddleware, headers=True, querystrings=True)
+    app.add_middleware(TotalTimeMiddleware)
 
-# if api_settings.lower_case_query_parameters:
-#     app.add_middleware(LowerCaseQueryStringMiddleware)
+if api_settings.lower_case_query_parameters:
+    app.add_middleware(LowerCaseQueryStringMiddleware)
 
 
 @app.get("/healthz", description="Health Check", tags=["Health Check"])
@@ -99,9 +96,9 @@ def landing(request: Request):
     )
 
 
-def main(
+async def main(
     req: func.HttpRequest,
     context: func.Context,
 ) -> func.HttpResponse:
     """Run App in AsgiMiddleware."""
-    return func.AsgiMiddleware(app).handle(req, context)
+    return await func.AsgiMiddleware(app).handle_async(req, context)
diff --git a/deployment/azure/host.json b/deployment/azure/host.json
index 8e588272b..6e86c559b 100644
--- a/deployment/azure/host.json
+++ b/deployment/azure/host.json
@@ -10,7 +10,7 @@
   },
   "extensionBundle": {
     "id": "Microsoft.Azure.Functions.ExtensionBundle",
-    "version": "[2.*, 3.0.0)"
+    "version": "[3.*, 4.0.0)"
   },
   "extensions": {
     "http": {
diff --git a/deployment/k8s/charts/Chart.yaml b/deployment/k8s/charts/Chart.yaml
index febe06e3e..5d964516c 100644
--- a/deployment/k8s/charts/Chart.yaml
+++ b/deployment/k8s/charts/Chart.yaml
@@ -1,8 +1,8 @@
 apiVersion: v1
-appVersion: 0.11.7
+appVersion: 0.22.4
 description: A dynamic Web Map tile server
 name: titiler
-version: 1.1.0
+version: 1.2.7
 icon: https://raw.githubusercontent.com/developmentseed/titiler/main/docs/logos/TiTiler_logo_small.png
 maintainers:
   - name: emmanuelmathot  # Emmanuel Mathot
diff --git a/deployment/k8s/charts/templates/deployment.yaml b/deployment/k8s/charts/templates/deployment.yaml
index 9858c13a6..ed4582249 100644
--- a/deployment/k8s/charts/templates/deployment.yaml
+++ b/deployment/k8s/charts/templates/deployment.yaml
@@ -14,10 +14,19 @@ spec:
       labels:
         {{- include "titiler.selectorLabels" . | nindent 8 }}
     spec:
+      securityContext:
+        {{- toYaml .Values.podSecurityContext | nindent 8 }}
       containers:
         - name: {{ .Chart.Name }}
           image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
           imagePullPolicy: {{ .Values.image.pullPolicy }}
+          command: [ {{ .Values.image.command }} ]
+          args: [ {{- range $arg := .Values.image.args }}
+            {{- $arg | quote }},
+            {{- end }}
+          ]
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
           env:
           {{- range $key, $val := .Values.env }}
             - name: {{ $key }}
@@ -33,7 +42,7 @@ spec:
           {{- end }}
           ports:
             - name: http
-              containerPort: {{ .Values.env.PORT }}
+              containerPort: 80
               protocol: TCP
           livenessProbe:
             httpGet:
@@ -49,10 +58,31 @@ spec:
             - mountPath: /config
               name: config
               readOnly: true
+          {{- range .Values.extraHostPathMounts }}
+            - name: {{ .name }}
+              mountPath: {{ .mountPath }}
+              readOnly: {{ .readOnly }}
+            {{- if .mountPropagation }}
+              mountPropagation: {{ .mountPropagation }}
+            {{- end }}
+          {{- end }}
+      terminationGracePeriodSeconds: {{ .Values.env.terminationGracePeriodSeconds }}
       volumes:
         - name: config
           configMap:
             name: {{ include "titiler.fullname" . }}-configmap
+      {{- range .Values.extraHostPathMounts }}
+        - name: {{ .name }}
+          hostPath:
+            path: {{ .hostPath }}
+            type: Directory
+      {{- end }}
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- range . }}
+      - name: {{ .name }}
+        {{- end }}
+      {{- end }}
       {{- with .Values.serviceAccountName }}
       serviceAccountName: {{ . | quote }}
       {{- end }}
diff --git a/deployment/k8s/charts/values-test.yaml b/deployment/k8s/charts/values-test.yaml
deleted file mode 100644
index 47bd601e4..000000000
--- a/deployment/k8s/charts/values-test.yaml
+++ /dev/null
@@ -1,44 +0,0 @@
-# Default values for titiler.
-replicaCount: 4
-
-ingress:
-  enabled: true
-  annotations: {}
-    # kubernetes.io/ingress.class: nginx
-    # kubernetes.io/tls-acme: "true"
-  hosts:
-    - host: titiler.charter.uat.esaportal.eu
-      paths: ["/"]
-  tls:
-   - secretName: domain-tls
-     hosts:
-       - titiler.charter.uat.esaportal.eu
-
-env:
-  PORT: 80
-  CPL_TMPDIR: /tmp
-  GDAL_CACHEMAX: 75%
-  VSI_CACHE: TRUE
-  VSI_CACHE_SIZE: 1073741824
-  GDAL_DISABLE_READDIR_ON_OPEN: EMPTY_DIR
-  GDAL_HTTP_MERGE_CONSECUTIVE_RANGES: YES
-  GDAL_HTTP_MULTIPLEX: YES
-  GDAL_HTTP_VERSION: 2
-  PYTHONWARNINGS: ignore
-  WEB_CONCURRENCY: 2
-
-resources:
-   limits:
-    cpu: 256m
-    memory: 1Gi
-    # ephemeral-storage: 10Gi
-   requests:
-    cpu: 256m
-    memory: 1Gi
-    # ephemeral-storage: 10Gi
-
-nodeSelector: {}
-
-tolerations: []
-
-affinity: {}
diff --git a/deployment/k8s/charts/values.yaml b/deployment/k8s/charts/values.yaml
index b10995183..2ccf50df0 100644
--- a/deployment/k8s/charts/values.yaml
+++ b/deployment/k8s/charts/values.yaml
@@ -2,13 +2,24 @@
 replicaCount: 1
 
 image:
-  repository: ghcr.io/developmentseed/titiler-uvicorn
+  repository: ghcr.io/developmentseed/titiler
   tag: latest
   pullPolicy: IfNotPresent
+  command: "uvicorn"
+  args:
+    - "titiler.application.main:app"
+    - "--host"
+    - "0.0.0.0"
+    - "--port"
+    - "80"
+    - "--workers"
+    - "1"
 
 nameOverride: ""
 fullnameOverride: ""
 
+terminationGracePeriodSeconds: 30
+
 service:
   type: ClusterIP
   port: 80
@@ -26,8 +37,16 @@ ingress:
   #    hosts:
   #      - titiler.local
 
+extraHostPathMounts: []
+  # - name: map-sources
+  #   mountPath: /map-sources/
+  #   hostPath: /home/ubuntu/map-sources
+  #   readOnly: false
+  #   mountPropagation: HostToContainer # OPTIONAL
+
+imagePullSecrets: []
+
 env:
-  PORT: 80
   CPL_TMPDIR: /tmp
   GDAL_CACHEMAX: 200  # 200 mb
   VSI_CACHE: "TRUE"
@@ -37,7 +56,6 @@ env:
   GDAL_HTTP_MULTIPLEX: "YES"
   GDAL_HTTP_VERSION: 2
   PYTHONWARNINGS: "ignore"
-  WEB_CONCURRENCY: 2
 
 resources:
   limits:
@@ -54,3 +72,17 @@ nodeSelector: {}
 tolerations: []
 
 affinity: {}
+
+securityContext: {}
+  # capabilities:
+  #   drop:
+  #     - ALL
+  # readOnlyRootFilesystem: true
+  # allowPrivilegeEscalation: false
+  # runAsNonRoot: true
+  # runAsUser: 1001
+
+podSecurityContext: {}
+  # fsGroup: 1001
+  # runAsNonRoot: true
+  # runAsUser: 1001
diff --git a/docker-compose.yml b/docker-compose.yml
index 9e3207e76..a06ac9854 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,23 +1,15 @@
-version: '3'
-
 services:
   titiler:
+    # TODO: remove once https://github.com/rasterio/rasterio-wheels/issues/69 is resolved
     # See https://github.com/developmentseed/titiler/discussions/387
     platform: linux/amd64
     build:
       context: .
-      dockerfile: dockerfiles/Dockerfile.gunicorn
+      dockerfile: dockerfiles/Dockerfile
     ports:
       - "8000:8000"
+    command: ["uvicorn", "titiler.application.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
     environment:
-      # Application
-      - HOST=0.0.0.0
-      - PORT=8000
-      # Gunicorn / Uvicorn
-      # https://github.com/tiangolo/uvicorn-gunicorn-docker#web_concurrency
-      - WEB_CONCURRENCY=1
-      # https://github.com/tiangolo/uvicorn-gunicorn-docker#workers_per_core
-      - WORKERS_PER_CORE=1
       # GDAL config
       - CPL_TMPDIR=/tmp
       - GDAL_CACHEMAX=75%
@@ -45,23 +37,25 @@ services:
       # - MOSAIC_CONCURRENCY= # will default to `RIO_TILER_MAX_THREADS`
       # rio-tiler config
       # - RIO_TILER_MAX_THREADS=
+      # telemetry config
+      - TITILER_API_TELEMETRY_ENABLED=True
+      - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
+    depends_on:
+      - otel-collector
+    volumes:
+      - ./:/data
 
-  titiler-uvicorn:
-    # See https://github.com/developmentseed/titiler/discussions/387
-    platform: linux/amd64
-    build:
-      context: .
-      dockerfile: dockerfiles/Dockerfile.uvicorn
+  titiler-gunicorn:
+    extends:
+      service: titiler
     ports:
       - "8000:8000"
+    command: ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "titiler.application.main:app", "--bind", "0.0.0.0:8000", "--workers", "1"]
+
+  benchmark:
+    extends:
+      service: titiler
     environment:
-      # Application
-      - HOST=0.0.0.0
-      - PORT=8000
-      # Uvicorn
-      # http://www.uvicorn.org/settings/#production
-      - WEB_CONCURRENCY=1
-      # GDAL config
       - CPL_TMPDIR=/tmp
       - GDAL_CACHEMAX=75%
       - GDAL_INGESTED_BYTES_AT_OPEN=32768
@@ -72,44 +66,52 @@ services:
       - PYTHONWARNINGS=ignore
       - VSI_CACHE=TRUE
       - VSI_CACHE_SIZE=536870912
-      # GDAL VSI Config
-      # https://gdal.org/user/virtual_file_systems.html#vsis3-aws-s3-files
-      # https://gdal.org/user/virtual_file_systems.html#vsigs-google-cloud-storage-files
-      # https://gdal.org/user/virtual_file_systems.html#vsiaz-microsoft-azure-blob-files
-      # - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
-      # - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
-      # TiTiler config
-      # - TITILER_API_DISABLE_STAC=TRUE/FALSE
-      # - TITILER_API_DISABLE_MOSAIC=TRUE/FALSE
-      # - TITILER_API_DISABLE_COG=TRUE/FALSE
-      # - TITILER_API_CORS_ORIGIN=url.io,url.xyz
-      # - TITILER_API_CACHECONTROL=public, max-age=3600
-      # - TITILER_API_DEBUG=TRUE/FALSE
-      # - MOSAIC_CONCURRENCY= # will default to `RIO_TILER_MAX_THREADS`
-      # rio-tiler config
-      # - RIO_TILER_MAX_THREADS=
-
-  benchmark:
-    extends:
-      service: titiler-uvicorn
+      - TITILER_API_TELEMETRY_ENABLED=False
     volumes:
       - ./.github/data:/data
 
   nginx-titiler:
     extends:
       service: titiler
-    ports:
-      - 8081:8081
-    environment:
-      - PORT=8081
-      - TITILER_API_ROOT_PATH=/api/v1/titiler
+    command: ["uvicorn", "titiler.application.main:app", "--host", "0.0.0.0", "--port", "8081", "--workers", "1", "--proxy-headers", "--forwarded-allow-ips='*'", "--root-path=/api/v1/titiler"]
 
   nginx:
     image: nginx
     ports:
-      - 80:80
+      - 8080:80
     volumes:
       - ./dockerfiles/nginx.conf:/etc/nginx/nginx.conf
     depends_on:
       - nginx-titiler
     command: ["nginx-debug", "-g", "daemon off;"]
+
+  titiler-xarray:
+    extends:
+      service: titiler
+    platform: linux/amd64
+    build:
+      context: .
+      dockerfile: dockerfiles/Dockerfile.xarray
+    ports:
+      - "8082:8082"
+    command: ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8082", "--workers", "1"]
+
+  otel-collector:
+    image: otel/opentelemetry-collector-contrib:latest
+    command: ["--config=/etc/otel-collector-config.yaml"]
+    volumes:
+      - ./dockerfiles/otel-collector-config.yaml:/etc/otel-collector-config.yaml
+    ports:
+      - "4318:4318"   # OTLP HTTP receiver
+      - "13133:13133" # Health check extension
+    depends_on:
+      - jaeger
+
+  jaeger:
+    image: jaegertracing/all-in-one:latest
+    ports:
+      - "16686:16686" # UI
+      - "14250:14250" # OTLP gRPC (Jaeger receiver)
+    environment:
+      - COLLECTOR_OTLP_ENABLED=true
+
diff --git a/dockerfiles/Dockerfile.uvicorn b/dockerfiles/Dockerfile
similarity index 54%
rename from dockerfiles/Dockerfile.uvicorn
rename to dockerfiles/Dockerfile
index e0a861133..923512477 100644
--- a/dockerfiles/Dockerfile.uvicorn
+++ b/dockerfiles/Dockerfile
@@ -1,5 +1,4 @@
-# Dockerfile for running titiler application with uvicorn server
-ARG PYTHON_VERSION=3.11
+ARG PYTHON_VERSION=3.12
 
 FROM bitnami/python:${PYTHON_VERSION}
 RUN apt update && apt upgrade -y \
@@ -12,13 +11,20 @@ RUN mkdir /usr/local/share/ca-certificates/cacert.org
 RUN cd /usr/local/share/ca-certificates/cacert.org && curl -k -O https://www.cacert.org/certs/root.crt
 RUN cd /usr/local/share/ca-certificates/cacert.org && curl -k -O https://www.cacert.org/certs/class3.crt
 RUN update-ca-certificates
-ENV CURL_CA_BUNDLE /etc/ssl/certs/ca-certificates.crt
+ENV CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
 
-COPY src/titiler/ /tmp/titiler/
 RUN python -m pip install -U pip
-RUN python -m pip install /tmp/titiler/core /tmp/titiler/extensions["cogeo,stac"] /tmp/titiler/mosaic /tmp/titiler/application["server"] --no-cache-dir --upgrade
+RUN python -m pip install uvicorn uvicorn-worker gunicorn
+
+COPY src/titiler/ /tmp/titiler/
+RUN python -m pip install /tmp/titiler/core /tmp/titiler/extensions["cogeo,stac"] /tmp/titiler/mosaic /tmp/titiler/application --no-cache-dir --upgrade
 RUN rm -rf /tmp/titiler
 
-ENV HOST 0.0.0.0
-ENV PORT 80
-CMD uvicorn titiler.application.main:app --host ${HOST} --port ${PORT}
+###################################################
+# For compatibility (might be removed at one point)
+ENV MODULE_NAME=titiler.application.main
+ENV VARIABLE_NAME=app
+ENV HOST=0.0.0.0
+ENV PORT=80
+ENV WEB_CONCURRENCY=1
+CMD gunicorn -k uvicorn.workers.UvicornWorker ${MODULE_NAME}:${VARIABLE_NAME} --bind ${HOST}:${PORT} --workers ${WEB_CONCURRENCY}
diff --git a/dockerfiles/Dockerfile.gunicorn b/dockerfiles/Dockerfile.gunicorn
deleted file mode 100644
index 6df319f04..000000000
--- a/dockerfiles/Dockerfile.gunicorn
+++ /dev/null
@@ -1,21 +0,0 @@
-# Dockerfile for running titiler application with gunicorn server
-# Size ~1.4GB
-ARG PYTHON_VERSION=3.11
-
-FROM ghcr.io/vincentsarago/uvicorn-gunicorn:${PYTHON_VERSION}
-
-# Ensure root certificates are always updated at evey container build
-# and curl is using the latest version of them
-RUN mkdir /usr/local/share/ca-certificates/cacert.org
-RUN cd /usr/local/share/ca-certificates/cacert.org && curl -k -O https://www.cacert.org/certs/root.crt
-RUN cd /usr/local/share/ca-certificates/cacert.org && curl -k -O https://www.cacert.org/certs/class3.crt
-RUN update-ca-certificates
-ENV CURL_CA_BUNDLE /etc/ssl/certs/ca-certificates.crt
-
-COPY src/titiler/ /tmp/titiler/
-RUN python -m pip install -U pip
-RUN python -m pip install /tmp/titiler/core /tmp/titiler/extensions["cogeo,stac"] /tmp/titiler/mosaic /tmp/titiler/application --no-cache-dir --upgrade
-RUN rm -rf /tmp/titiler
-
-ENV MODULE_NAME titiler.application.main
-ENV VARIABLE_NAME app
diff --git a/dockerfiles/Dockerfile.xarray b/dockerfiles/Dockerfile.xarray
new file mode 100644
index 000000000..e0e13834c
--- /dev/null
+++ b/dockerfiles/Dockerfile.xarray
@@ -0,0 +1,24 @@
+ARG PYTHON_VERSION=3.12
+
+FROM bitnami/python:${PYTHON_VERSION}
+RUN apt update && apt upgrade -y \
+  && apt install curl -y \
+  && rm -rf /var/lib/apt/lists/*
+
+RUN python -m pip install -U pip
+RUN python -m pip install uvicorn uvicorn-worker gunicorn
+
+COPY src/titiler/ /tmp/titiler/
+RUN python -m pip install /tmp/titiler/core["telemetry"] "/tmp/titiler/xarray[full]" starlette-cramjam --no-cache-dir --upgrade
+RUN rm -rf /tmp/titiler
+
+COPY src/titiler/xarray/examples/main.py app.py
+
+###################################################
+# For compatibility (might be removed at one point)
+ENV MODULE_NAME=app
+ENV VARIABLE_NAME=app
+ENV HOST=0.0.0.0
+ENV PORT=80
+ENV WEB_CONCURRENCY=1
+CMD gunicorn -k uvicorn.workers.UvicornWorker ${MODULE_NAME}:${VARIABLE_NAME} --bind ${HOST}:${PORT} --workers ${WEB_CONCURRENCY}
diff --git a/dockerfiles/nginx.conf b/dockerfiles/nginx.conf
index ccf17b175..12ad20609 100644
--- a/dockerfiles/nginx.conf
+++ b/dockerfiles/nginx.conf
@@ -6,8 +6,8 @@ http {
 
         location /api/v1/titiler {
             rewrite ^/api/v1/titiler(.*)$ $1 break;
-            proxy_pass http://app:8081;
-            proxy_set_header HOST $host;
+            proxy_pass http://nginx-titiler:8081;
+            proxy_set_header HOST $http_host;
             proxy_set_header Referer $http_referer;
             proxy_set_header X-Forwarded-For $remote_addr;
             proxy_set_header X-Forwarded-Proto $scheme;
diff --git a/dockerfiles/otel-collector-config.yaml b/dockerfiles/otel-collector-config.yaml
new file mode 100644
index 000000000..fed63867f
--- /dev/null
+++ b/dockerfiles/otel-collector-config.yaml
@@ -0,0 +1,28 @@
+receivers:
+  otlp:
+    protocols:
+      http:
+        endpoint: 0.0.0.0:4318
+
+processors:
+  batch:
+
+exporters:
+  otlp:
+    endpoint: jaeger:4317
+    tls:
+      insecure: true
+  debug:
+    verbosity: detailed
+
+extensions:
+  health_check:
+    endpoint: 0.0.0.0:13133
+
+service:
+  extensions: [health_check]
+  pipelines:
+    traces:
+      receivers: [otlp]
+      processors: [batch]
+      exporters: [otlp, debug]
\ No newline at end of file
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 1d3aa53f7..3c5d0ad38 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -10,6 +10,22 @@ edit_uri: "blob/main/docs/src/"
 site_url: "https://developmentseed.org/titiler/"
 
 extra:
+  analytics:
+    provider: plausible
+    domain: developmentseed.org/titiler
+
+    feedback:
+      title: Was this page helpful?
+      ratings:
+        - icon: material/emoticon-happy-outline
+          name: This page was helpful
+          data: good
+          note: Thanks for your feedback!
+
+        - icon: material/emoticon-sad-outline
+          name: This page could be improved
+          data: bad
+          note: Thanks for your feedback!
   social:
     - icon: "fontawesome/brands/github"
       link: "https://github.com/developmentseed"
@@ -21,22 +37,36 @@ extra:
 nav:
   - TiTiler: "index.md"
   - User Guide:
-    - Intro: "intro.md"
-    - Dynamic Tiling: "dynamic_tiling.md"
-    - Mosaics: "mosaics.md"
-    - TileMatrixSets: "tile_matrix_sets.md"
-    - Output data format: "output_format.md"
+    - Getting Started: "user_guide/getting_started.md"
+    - Dynamic Tiling: "user_guide/dynamic_tiling.md"
+    - TileMatrixSets: "user_guide/tile_matrix_sets.md"
+    - Output data format: "user_guide/output_format.md"
+    - Algorithm: "user_guide/algorithms.md"
+    - Rendering: "user_guide/rendering.md"
 
   - Advanced User Guide:
-      - Tiler Factories: "advanced/tiler_factories.md"
+      - Endpoints Factories: "advanced/endpoints_factories.md"
       - Dependencies: "advanced/dependencies.md"
       - Customization: "advanced/customization.md"
       - Performance Tuning: "advanced/performance_tuning.md"
-      - Custom Algorithm: "advanced/Algorithms.md"
       - Extensions: "advanced/Extensions.md"
-      - Rendering: "advanced/rendering.md"
       # - APIRoute and environment variables: "advanced/APIRoute_and_environment_variables.md"
 
+  - Packages:
+    - titiler.core: "packages/core.md"
+    - titiler.xarray: "packages/xarray.md"
+    - titiler.extensions: "packages/extensions.md"
+    - titiler.mosaic: "packages/mosaic.md"
+    - titiler.application: "packages/application.md"
+
+  - Endpoints documentation:
+    - /cog: "endpoints/cog.md"
+    - /stac: "endpoints/stac.md"
+    - /mosaicjson: "endpoints/mosaic.md"
+    - /tileMatrixSets: "endpoints/tms.md"
+    - /algorithms: "endpoints/algorithms.md"
+    - /colormaps: "endpoints/colormaps.md"
+
   - Examples:
     - Create dynamic tilers with TiTiler:
       - Minimal COG Tiler: "examples/code/mini_cog_tiler.md"
@@ -50,6 +80,8 @@ nav:
       - Custom Sentinel 2 Tiler: "examples/code/tiler_for_sentinel2.md"
       - Add custom algorithms: "examples/code/tiler_with_custom_algorithm.md"
       - GDAL WMTS Extension: "examples/code/create_gdal_wmts_extension.md"
+      - STAC + Xarray: "examples/code/tiler_with_custom_stac+xarray.md"
+      - Custom Layers: "examples/code/tiler_with_layers.md"
 
     - Use TiTiler endpoints:
       - COG: "examples/notebooks/Working_with_CloudOptimizedGeoTIFF_simple.ipynb"
@@ -68,27 +100,32 @@ nav:
       - factory: api/titiler/core/factory.md
       - routing: api/titiler/core/routing.md
       - errors: api/titiler/core/errors.md
-      - enums: api/titiler/core/resources/enums.md
       - middleware: api/titiler/core/middleware.md
+      - resources:
+        - enums: api/titiler/core/resources/enums.md
+        - responses: api/titiler/core/resources/responses.md
+      - models:
+        - OGC: api/titiler/core/models/OGC.md
+        - Mapbox/MapLibre: api/titiler/core/models/mapbox.md
+        - responses: api/titiler/core/models/responses.md
     - titiler.extensions:
       - cogeo: api/titiler/extensions/cogeo.md
       - stac: api/titiler/extensions/stac.md
       - viewer: api/titiler/extensions/viewer.md
     - titiler.mosaic:
       - factory: api/titiler/mosaic/factory.md
-      - enums: api/titiler/mosaic/resources/enums.md
       - errors: api/titiler/mosaic/errors.md
-
-  - titiler.application:
-    - /cog: "endpoints/cog.md"
-    - /stac: "endpoints/stac.md"
-    - /mosaicjson: "endpoints/mosaic.md"
-    - /tileMatrixSets: "endpoints/tms.md"
+      - models:
+        - responses: api/titiler/mosaic/models/responses.md
+    - titiler.xarray:
+      - io: api/titiler/xarray/io.md
+      - dependencies: api/titiler/xarray/dependencies.md
+      - extensions: api/titiler/xarray/extensions.md
+      - factory: api/titiler/xarray/factory.md
 
   - Deployment:
     - Amazon Web Services:
       - Intro: "deployment/aws/intro.md"
-      - ECS: "deployment/aws/ecs.md"
       - Lambda: "deployment/aws/lambda.md"
       - SAM: "deployment/aws/sam.md"
     - k8s / Helm Deployment: "deployment/k8s.md"
@@ -97,20 +134,81 @@ nav:
   - External links: "external_links.md"
   - Development - Contributing: "contributing.md"
   - Release Notes: "release-notes.md"
+  - Security: "security.md"
+  - Performance Benchmarks: benchmark.html
 
 plugins:
   - search
+  - social
   - mkdocs-jupyter:
-      include_source: True
+      include_source: true
+      ignore: ["**/.ipynb_checkpoints/*.ipynb"]
+  - mkdocstrings:
+      enable_inventory: true
+      handlers:
+        python:
+          paths: [src]
+          options:
+            filters:
+            - "!^__post_init__"
+            docstring_section_style: list
+            docstring_style: google
+            line_length: 100
+            separate_signature: true
+            show_root_heading: true
+            show_signature_annotations: true
+            show_source: false
+            show_symbol_type_toc: true
+            signature_crossrefs: true
+            extensions:
+              - griffe_inherited_docstrings
+          inventories:
+            - https://docs.python.org/3/objects.inv
+            - https://numpy.org/doc/stable/objects.inv
+            - https://rasterio.readthedocs.io/en/stable/objects.inv
+            - https://docs.pydantic.dev/latest/objects.inv
+            - https://fastapi.tiangolo.com/objects.inv
+            - https://cogeotiff.github.io/rio-tiler/objects.inv
 
 theme:
   name: material
   palette:
-    primary: indigo
-    scheme: default
+    # Palette toggle for automatic mode
+    - media: "(prefers-color-scheme)"
+      toggle:
+        icon: material/brightness-auto
+        name: Switch to light mode
+
+    # Palette toggle for light mode
+    - media: "(prefers-color-scheme: light)"
+      scheme: default
+      primary: indigo
+      accent: indigo
+      toggle:
+          icon: material/brightness-7
+          name: Switch to dark mode
+
+    # Palette toggle for dark mode
+    - media: "(prefers-color-scheme: dark)"
+      scheme: slate
+      primary: indigo
+      accent: indigo
+      toggle:
+          icon: material/brightness-4
+          name: Switch to light mode
+
   custom_dir: 'src/overrides'
   favicon: img/favicon.png
 
+  features:
+    - content.code.annotate
+    - content.code.copy
+    - navigation.indexes
+    - navigation.instant
+    - navigation.tracking
+    - search.suggest
+    - search.share
+
 # https://github.com/kylebarron/cogeo-mosaic/blob/mkdocs/mkdocs.yml#L50-L75
 markdown_extensions:
   - admonition
@@ -124,7 +222,9 @@ markdown_extensions:
   - pymdownx.caret:
       insert: false
   - pymdownx.details
-  - pymdownx.emoji
+  - pymdownx.emoji:
+      emoji_index: !!python/name:material.extensions.emoji.twemoji
+      emoji_generator: !!python/name:material.extensions.emoji.to_svg
   - pymdownx.escapeall:
       hardbreak: true
       nbsp: true
diff --git a/docs/src/advanced/Extensions.md b/docs/src/advanced/Extensions.md
index 9af9a7c2f..6c2d70b67 100644
--- a/docs/src/advanced/Extensions.md
+++ b/docs/src/advanced/Extensions.md
@@ -33,28 +33,36 @@ class FactoryExtension(metaclass=abc.ABCMeta):
 
 ## Available extensions
 
-#### cogValidateExtension
+#### titiler.extensions.cogValidateExtension
 
 - Goal: adds a `/validate` endpoint which return the content of rio-cogeo `info` method
 - Additional requirements: `titiler.extensions["cogeo"]` (installs `rio-cogeo`)
 
-#### cogViewerExtension
+#### titiler.extensions.cogViewerExtension
 
 - Goal: adds a `/viewer` endpoint which return an HTML viewer for simple COGs
 
-#### stacViewerExtension
+#### titiler.extensions.stacViewerExtension
 
 - Goal: adds a `/viewer` endpoint which return an HTML viewer for STAC item
 
-#### stacExtension
+#### titiler.extensions.stacExtension
 
 - Goal: adds a `/stac` endpoint which return an HTML viewer for STAC item
 - Additional requirements: `titiler.extensions["stac"]` (installs `rio-stac`)
 
-#### wmsExtension
+#### titiler.extensions.wmsExtension
 
 - Goal: adds a `/wms` endpoint to support OGC WMS specification (`GetCapabilities` and `GetMap`)
 
+#### titiler.extensions.stacRenderExtenstion
+
+- Goal: adds `/render` and `/render/{render_id}` endpoints which return the contents of [STAC render extension](https://github.com/stac-extensions/render) and links to tileset.json and WMTS service
+
+#### titiler.xarray.DatasetMetadataExtension
+
+- Goal: adds `/dataset/`, `/dataset/keys` and `/datasets/dict` endpoints which return metadata about a multidimensional Dataset (not a DataArray)
+
 ## How To
 
 ### Use extensions
@@ -81,18 +89,16 @@ tiler = TilerFactory(
 app.include_router(tiler.router, prefix="/cog")
 ```
 
-See [titiler.application](../application) for a full example.
-
 ### Create your own
 
 ```python
 from dataclasses import dataclass, field
 from typing import Tuple, List, Optional
-
+import rasterio
 from starlette.responses import Response
 from fastapi import Depends, FastAPI, Query
-from titiler.core.factory import BaseTilerFactory, FactoryExtension, TilerFactory
-from titiler.core.dependencies import RescalingParams
+from titiler.core.factory import TilerFactory, FactoryExtension
+from titiler.core.dependencies import ImageRenderingParams
 from titiler.core.factory import TilerFactory
 from titiler.core.resources.enums import ImageType
 
@@ -104,8 +110,8 @@ class thumbnailExtension(FactoryExtension):
     # Set some options
     max_size: int = field(default=128)
 
-    # Register method is mandatory and must take a BaseTilerFactory object as input
-    def register(self, factory: BaseTilerFactory):
+    # Register method is mandatory and must take a TilerFactory object as input
+    def register(self, factory: TilerFactory):
         """Register endpoint to the tiler factory."""
 
         # register an endpoint to the factory's router
@@ -125,47 +131,37 @@ class thumbnailExtension(FactoryExtension):
         def thumbnail(
             # we can reuse the factory dependency
             src_path: str = Depends(factory.path_dependency),
+            reader_params=Depends(factory.reader_dependency),
             layer_params=Depends(factory.layer_dependency),
             dataset_params=Depends(factory.dataset_dependency),
             post_process=Depends(factory.process_dependency),
-            rescale: Optional[List[Tuple[float, ...]]] = Depends(RescalingParams),
-            color_formula: Optional[str] = Query(
-                None,
-                title="Color Formula",
-                description="rio-color formula (info: https://github.com/mapbox/rio-color)",
-            ),
             colormap=Depends(factory.colormap_dependency),
             render_params=Depends(factory.render_dependency),
-            reader_params=Depends(factory.reader_dependency),
             env=Depends(factory.environment_dependency),
         ):
             with rasterio.Env(**env):
-                with self.reader(src_path, **reader_params) as src_dst:
-                    im = src.preview(
+                with factory.reader(src_path, **reader_params.as_dict()) as src:
+                    image = src.preview(
                         max_size=self.max_size,
-                        **layer_params,
-                        **dataset_params,
+                        **layer_params.as_dict(),
+                        **dataset_params.as_dict(),
                     )
 
             if post_process:
                 image = post_process(image)
 
-            if rescale:
-                image.rescale(rescale)
-
-            if color_formula:
-                image.apply_color_formula(color_formula)
-
             format = ImageType.jpeg if image.mask.all() else ImageType.png
 
-            content = image.render(
-                img_format=format.driver,
-                colormap=colormap or dst_colormap,
-                **format.profile,
-                **render_params,
+            if post_process:
+                image = post_process(image)
+
+            content, media_type = factory.render_func(
+                image,
+                colormap=colormap,
+                **render_params.as_dict(),
             )
 
-            return Response(content, media_type=format.mediatype)
+            return Response(content, media_type=media_type)
 
 # Use it
 app = FastAPI()
diff --git a/docs/src/advanced/customization.md b/docs/src/advanced/customization.md
index 13bcbfaa6..0daabfbd7 100644
--- a/docs/src/advanced/customization.md
+++ b/docs/src/advanced/customization.md
@@ -1,6 +1,33 @@
 
 `TiTiler` is designed to help user customize input/output for each endpoint. This section goes over some simple customization examples.
 
+### Custom Colormap
+
+Add user defined colormap to the default colormaps provided by rio-tiler
+
+```python
+from fastapi import FastAPI
+
+from rio_tiler.colormap import cmap as default_cmap
+
+from titiler.core.dependencies import create_colormap_dependency
+from titiler.core.factory import TilerFactory
+
+
+app = FastAPI(title="My simple app with custom TMS")
+
+cmap_values = {
+    "cmap1": {6: (4, 5, 6, 255)},
+}
+# add custom colormap `cmap1` to the default colormaps
+cmap = default_cmap.register(cmap_values)
+ColorMapParams = create_colormap_dependency(cmap)
+
+
+cog = TilerFactory(colormap_dependency=ColorMapParams)
+app.include_router(cog.router)
+```
+
 ### Custom DatasetPathParams for `reader_dependency`
 
 One common customization could be to create your own `path_dependency`. This dependency is used on all endpoint and pass inputs to the *Readers* (MosaicBackend, COGReader, STACReader...).
@@ -13,7 +40,6 @@ import re
 
 from fastapi import FastAPI, HTTPException, Query
 
-from titiler.core.dependencies import DefaultDependency
 from titiler.mosaic.factory import MosaicTilerFactory
 
 
diff --git a/docs/src/advanced/dependencies.md b/docs/src/advanced/dependencies.md
index 937395e1f..094c7db90 100644
--- a/docs/src/advanced/dependencies.md
+++ b/docs/src/advanced/dependencies.md
@@ -5,31 +5,17 @@ In titiler `Factories`, we use the dependencies to define the inputs for each en
 
 Example:
 ```python
-# Custom Dependency
-
 from dataclasses import dataclass
-from typing import Optional
-
 from fastapi import Depends, FastAPI, Query
 from titiler.core.dependencies import DefaultDependency
-
-from rio_tiler.io import COGReader
+from typing_extensions import Annotated
+from rio_tiler.io import Reader
 
 @dataclass
 class ImageParams(DefaultDependency):
-    """Common Preview/Crop parameters."""
-
-    max_size: Optional[int] = Query(
-        1024, description="Maximum image size to read onto."
-    )
-    height: Optional[int] = Query(None, description="Force output image height.")
-    width: Optional[int] = Query(None, description="Force output image width.")
-
-    def __post_init__(self):
-        """Post Init."""
-        if self.width and self.height:
-            self.max_size = None
-
+    max_size: Annotated[
+        int, Query(description="Maximum image size to read onto.")
+    ] = 1024
 
 app = FastAPI()
 
@@ -39,15 +25,10 @@ def preview(
     url: str = Query(..., description="data set URL"),
     params: ImageParams = Depends(),
 ):
-
-    with COGReader(url) as cog:
-        img = cog.preview(**params)  # classes built with `DefaultDependency` can be unpacked
+    with Reader(url) as cog:
+        img = cog.preview(**params.as_dict())  # we use `DefaultDependency().as_dict()` to pass only non-None parameters
         # or
-        img = cog.preview(
-            max_size=params.max_size,
-            height=params.height,
-            width=params.width,
-        )
+        img = cog.preview(max_size=params.max_size)
     ...
 ```
 
@@ -55,238 +36,809 @@ def preview(
 
     In the example above, we create a custom `ImageParams` dependency which will then be injected to the `preview` endpoint to add  **max_size**, **height** and **width** query string parameters.
 
-    Using `titiler.core.dependencies.DefaultDependency`, we can `unpack` the class as if it was a dictionary, which helps with customization.
+    Using `titiler.core.dependencies.DefaultDependency`, we can use `.as_dict(exclude_none=True/False)` method to `unpack` the object parameters. This can be useful if method or reader do not take the same parameters.
+
+## titiler.core
+
+#### AssetsParams
+
+Define `assets`.
+
+| Name      | Type      | Required | Default
+| ------    | ----------|----------|--------------
+| **assets** | Query (str) | No | None
+
+
+ +```python +@dataclass +class AssetsParams(DefaultDependency): + """Assets parameters.""" + + assets: List[str] = Query( + None, + title="Asset names", + description="Asset's names.", + openapi_examples={ + "user-provided": {"value": None}, + "one-asset": { + "description": "Return results for asset `data`.", + "value": ["data"], + }, + "multi-assets": { + "description": "Return results for assets `data` and `cog`.", + "value": ["data", "cog"], + }, + }, + ) +``` +
-## Factories Dependencies -The `factories` allow users to set multiple default dependencies. Here is the list of common dependencies and their **default** values. +#### AssetsBidxParams -### BaseTilerFactory +Define `assets` with option of `per-asset` expression with `asset_expression` option. -#### path_dependency +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **assets** | Query (str) | No | None +| **asset_indexes** | Query (str) | No | None +| **asset_expression** | Query (str) | No | False -Set dataset path (url). +
```python -def DatasetPathParams( - url: str = Query(..., description="Dataset URL") -) -> str: - """Create dataset path from args""" - return url +@dataclass +class AssetsBidxParams(AssetsParams): + """Assets, Asset's band Indexes and Asset's band Expression parameters.""" + + asset_indexes: Annotated[ + Optional[Sequence[str]], + Query( + title="Per asset band indexes", + description="Per asset band indexes", + alias="asset_bidx", + openapi_examples={ + "user-provided": {"value": None}, + "one-asset": { + "description": "Return indexes 1,2,3 of asset `data`.", + "value": ["data|1;2;3"], + }, + "multi-assets": { + "description": "Return indexes 1,2,3 of asset `data` and indexes 1 of asset `cog`", + "value": ["data|1;2;3", "cog|1"], + }, + }, + ), + ] = None + + asset_expression: Annotated[ + Optional[Sequence[str]], + Query( + title="Per asset band expression", + description="Per asset band expression", + openapi_examples={ + "user-provided": {"value": None}, + "one-asset": { + "description": "Return results for expression `b1*b2+b3` of asset `data`.", + "value": ["data|b1*b2+b3"], + }, + "multi-assets": { + "description": "Return results for expressions `b1*b2+b3` for asset `data` and `b1+b3` for asset `cog`.", + "value": ["data|b1*b2+b3", "cog|b1+b3"], + }, + }, + ), + ] = None + + def __post_init__(self): + """Post Init.""" + if self.asset_indexes: + self.asset_indexes: Dict[str, Sequence[int]] = { # type: ignore + idx.split("|")[0]: list(map(int, idx.split("|")[1].split(","))) + for idx in self.asset_indexes + } + + if self.asset_expression: + self.asset_expression: Dict[str, str] = { # type: ignore + idx.split("|")[0]: idx.split("|")[1] for idx in self.asset_expression + } ``` -#### layer_dependency +
+ +#### AssetsBidxExprParams + +Define `assets`. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **assets** | Query (str) | No\* | None +| **expression** | Query (str) | No\* | None +| **asset_indexes** | Query (str) | No | None +| **asset_as_band** | Query (bool) | No | False + +\* `assets` or `expression` is required. -Define band indexes or expression +
```python @dataclass -class BidxParams(DefaultDependency): - """Band Indexes parameters.""" +class AssetsBidxExprParams(AssetsParams): + """Assets, Expression and Asset's band Indexes parameters.""" + + expression: Annotated[ + Optional[str], + Query( + title="Band Math expression", + description="Band math expression between assets", + openapi_examples={ + "user-provided": {"value": None}, + "simple": { + "description": "Return results of expression between assets.", + "value": "asset1_b1 + asset2_b1 / asset3_b1", + }, + }, + ), + ] = None + + asset_indexes: Annotated[ + Optional[Sequence[str]], + Query( + title="Per asset band indexes", + description="Per asset band indexes (coma separated indexes)", + alias="asset_bidx", + openapi_examples={ + "user-provided": {"value": None}, + "one-asset": { + "description": "Return indexes 1,2,3 of asset `data`.", + "value": ["data|1,2,3"], + }, + "multi-assets": { + "description": "Return indexes 1,2,3 of asset `data` and indexes 1 of asset `cog`", + "value": ["data|1,2,3", "cog|1"], + }, + }, + ), + ] = None - indexes: Optional[List[int]] = Query( - None, - title="Band indexes", - alias="bidx", - description="Dataset band indexes", - examples={"one-band": {"value": [1]}, "multi-bands": {"value": [1, 2, 3]}}, - ) + asset_as_band: Annotated[ + Optional[bool], + Query( + title="Consider asset as a 1 band dataset", + description="Asset as Band", + ), + ] = None + def __post_init__(self): + """Post Init.""" + if not self.assets and not self.expression: + raise MissingAssets( + "assets must be defined either via expression or assets options." + ) + + if self.asset_indexes: + self.asset_indexes: Dict[str, Sequence[int]] = { # type: ignore + idx.split("|")[0]: list(map(int, idx.split("|")[1].split(","))) + for idx in self.asset_indexes + } +``` + +
+ +#### AssetsBidxExprParamsOptional + +Define `assets`. Without requirement on assets nor expression. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **assets** | Query (str) | No | None +| **expression** | Query (str) | No | None +| **asset_indexes** | Query (str) | No | None +| **asset_as_band** | Query (bool) | No | False + +
+ +```python @dataclass -class ExpressionParams(DefaultDependency): - """Expression parameters.""" +class AssetsBidxExprParamsOptional(AssetsBidxExprParams): + """Assets, Expression and Asset's band Indexes parameters but with no requirement.""" + + def __post_init__(self): + """Post Init.""" + if self.asset_indexes: + self.asset_indexes: Dict[str, Sequence[int]] = { # type: ignore + idx.split("|")[0]: list(map(int, idx.split("|")[1].split(","))) + for idx in self.asset_indexes + } +``` + +
+ + +#### BandsParams + +Define `bands`. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **bands** | Query (str) | No | None - expression: Optional[str] = Query( +
+ +```python +@dataclass +class BandsParams(DefaultDependency): + """Band names parameters.""" + + bands: List[str] = Query( None, - title="Band Math expression", - description="rio-tiler's band math expression", - examples={ - "simple": {"description": "Simple band math.", "value": "b1/b2"}, + title="Band names", + description="Band's names.", + openapi_examples={ + "user-provided": {"value": None}, + "one-band": { + "description": "Return results for band `B01`.", + "value": ["B01"], + }, "multi-bands": { - "description": "Semicolon (;) delimited expressions (band1: b1/b2, band2: b2+b3).", - "value": "b1/b2;b2+b3", + "description": "Return results for bands `B01` and `B02`.", + "value": ["B01", "B02"], }, }, ) +``` + +
+ + +#### BandsExprParams + +Define `bands`. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **bands** | Query (str) | No\* | None +| **expression** | Query (str) | No\* | None + +\* `bands` or `expression` is required. +
+ +```python @dataclass -class BidxExprParams(ExpressionParams, BidxParams): - """Band Indexes and Expression parameters.""" +class BandsExprParamsOptional(ExpressionParams, BandsParams): + """Optional Band names and Expression parameters.""" pass ``` -#### dataset_dependency +
+ +#### BandsExprParamsOptional + +Define `bands`. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **bands** | Query (str) | No | None +| **expression** | Query (str) | No | None -Overwrite nodata value, apply rescaling or change default resampling. +
```python @dataclass -class DatasetParams(DefaultDependency): - """Low level WarpedVRT Optional parameters.""" +class BandsExprParamsOptional(ExpressionParams, BandsParams): + """Optional Band names and Expression parameters.""" - nodata: Optional[Union[str, int, float]] = Query( - None, title="Nodata value", description="Overwrite internal Nodata value" - ) - unscale: Optional[bool] = Query( - False, - title="Apply internal Scale/Offset", - description="Apply internal Scale/Offset", - ) - resampling_method: ResamplingName = Query( - ResamplingName.nearest, # type: ignore - alias="resampling", - description="Resampling method.", - ) + pass +``` - def __post_init__(self): - """Post Init.""" - if self.nodata is not None: - self.nodata = numpy.nan if self.nodata == "nan" else float(self.nodata) - self.resampling_method = self.resampling_method.value # type: ignore +
+ +#### `BidxParams` + +Define band indexes. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **bidx** | Query (int) | No | None + +
+ +```python +@dataclass +class BidxParams(DefaultDependency): + """Band Indexes parameters.""" + + indexes: Annotated[ + Optional[List[int]], + Query( + title="Band indexes", + alias="bidx", + description="Dataset band indexes", + openapi_examples={ + "user-provided": {"value": None}, + "one-band": {"value": [1]}, + "multi-bands": {"value": [1, 2, 3]}, + }, + ), + ] = None ``` -#### render_dependency +
-Image rendering options. +#### `ExpressionParams` + +Define band expression. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **expression** | Query (str) | No | None + + +
```python @dataclass -class ImageRenderingParams(DefaultDependency): - """Image Rendering options.""" +class ExpressionParams(DefaultDependency): + """Expression parameters.""" - add_mask: bool = Query( - True, alias="return_mask", description="Add mask to the output data." - ) + expression: Annotated[ + Optional[str], + Query( + title="Band Math expression", + description="rio-tiler's band math expression", + openapi_examples={ + "user-provided": {"value": None}, + "simple": {"description": "Simple band math.", "value": "b1/b2"}, + "multi-bands": { + "description": "Semicolon (;) delimited expressions (band1: b1/b2, band2: b2+b3).", + "value": "b1/b2;b2+b3", + }, + }, + ), + ] = None +``` + +
+ +#### `BidxExprParams` + +Define band indexes or expression. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **bidx** | Query (int) | No | None +| **expression** | Query (str) | No | None + +
+ +```python +@dataclass +class BidxExprParams(ExpressionParams, BidxParams): + """Band Indexes and Expression parameters.""" + + pass ``` -#### colormap_dependency +
-Colormap options. +#### `ColorMapParams` + +Colormap options. See [titiler.core.dependencies](https://github.com/developmentseed/titiler/blob/e46c35c8927b207f08443a274544901eb9ef3914/src/titiler/core/titiler/core/dependencies.py#L18-L54). + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **colormap_name** | Query (str) | No | None +| **colormap** | Query (encoded json) | No | None + +
```python +cmap = {} + def ColorMapParams( - colormap_name: ColorMapName = Query(None, description="Colormap name"), - colormap: str = Query(None, description="JSON encoded custom Colormap"), -) -> Optional[Union[Dict, Sequence]]: - """Colormap Dependency.""" + colormap_name: Annotated[ # type: ignore + Literal[tuple(cmap.list())], + Query(description="Colormap name"), + ] = None, + colormap: Annotated[ + Optional[str], Query(description="JSON encoded custom Colormap") + ] = None, +): if colormap_name: - return cmap.get(colormap_name.value) + return cmap.get(colormap_name) if colormap: try: - return json.loads( + c = json.loads( colormap, - object_hook=lambda x: {int(k): parse_color(v) for k, v in x.items()}, + object_hook=lambda x: { + int(k): parse_color(v) for k, v in x.items() + }, ) - except json.JSONDecodeError: + + # Make sure to match colormap type + if isinstance(c, Sequence): + c = [(tuple(inter), parse_color(v)) for (inter, v) in c] + + return c + except json.JSONDecodeError as e: raise HTTPException( status_code=400, detail="Could not parse the colormap value." - ) + ) from e return None ``` -#### reader_dependency +
+ +#### CoordCRSParams + +Define input Coordinate Reference System. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **crs** | Query (str) | No | None + + +
+ +```python +def CoordCRSParams( + crs: Annotated[ + Optional[str], + Query( + alias="coord_crs", + description="Coordinate Reference System of the input coords. Default to `epsg:4326`.", + ), + ] = None, +) -> Optional[CRS]: + """Coordinate Reference System Coordinates Param.""" + if crs: + return CRS.from_user_input(crs) + + return None +``` + +
+ +#### `DatasetParams` + +Overwrite `nodata` value, apply `rescaling` and change the `I/O` or `Warp` resamplings. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **nodata** | Query (str, int, float) | No | None +| **unscale** | Query (bool) | No | False +| **resampling** | Query (str) | No | 'nearest' +| **reproject** | Query (str) | No | 'nearest' + +
+ +```python +@dataclass +class DatasetParams(DefaultDependency): + """Low level WarpedVRT Optional parameters.""" + + nodata: Annotated[ + Optional[Union[str, int, float]], + Query( + title="Nodata value", + description="Overwrite internal Nodata value", + ), + ] = None + unscale: Annotated[ + bool, + Query( + title="Apply internal Scale/Offset", + description="Apply internal Scale/Offset. Defaults to `False` in rio-tiler.", + ), + ] = False + resampling_method: Annotated[ + Optional[RIOResampling], + Query( + alias="resampling", + description="RasterIO resampling algorithm. Defaults to `nearest` in rio-tiler.", + ), + ] = None + reproject_method: Annotated[ + Optional[WarpResampling], + Query( + alias="reproject", + description="WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest` in rio-tiler.", + ), + ] = None + + def __post_init__(self): + """Post Init.""" + if self.nodata is not None: + self.nodata = numpy.nan if self.nodata == "nan" else float(self.nodata) + + if self.unscale is not None: + self.unscale = bool(self.unscale) +``` + +
-Additional reader options. Defaults to `DefaultDependency` (empty). +#### `DatasetPathParams` +Set dataset path. -#### Other Attributes +| Name | Type | Required | Default +| ------ | ----------|--------------------- |-------------- +| **url** | Query (str) | :warning: **Yes** :warning: | - -##### Supported TMS -The TMS dependency sets the available TMS for a tile endpoint. +
```python -# Allow all morecantile TMS -from morecantile import tms as default_tms +def DatasetPathParams( + url: Annotated[str, Query(description="Dataset URL")] +) -> str: + """Create dataset path from args""" + return url +``` + +
+ + +#### DstCRSParams + +Define output Coordinate Reference System. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **crs** | Query (str) | No | None -tiler = TilerFactory(supported_tms=default_tms) +
-# Restrict the TMS to `WebMercatorQuad` only -from morecantile import tms -from morecantile.defaults import TileMatrixSets +```python +def DstCRSParams( + crs: Annotated[ + Optional[str], + Query( + alias="dst_crs", + description="Output Coordinate Reference System.", + ), + ] = None, +) -> Optional[CRS]: + """Coordinate Reference System Coordinates Param.""" + if crs: + return CRS.from_user_input(crs) -# Construct a TileMatrixSets object with only the `WebMercatorQuad` tms -default_tms = TileMatrixSets({"WebMercatorQuad": tms.get("WebMercatorQuad")}) -tiler = TilerFactory(supported_tms=default_tms) + return None ``` -##### Default TMS +
+ +#### HistogramParams -Set the default's TMS Identifier (default to `WebMercatorQuad`). +Define *numpy*'s histogram options. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **histogram_bins** | Query (encoded list of Number) | No | 10 +| **histogram_range** | Query (encoded list of Number) | No | None + +
```python -# Create a Tile with it's default TMS being `WGS1984Quad` -tiler = TilerFactory(default_tms="WGS1984Quad") +@dataclass +class HistogramParams(DefaultDependency): + """Numpy Histogram options.""" + + bins: Annotated[ + Optional[str], + Query( + alias="histogram_bins", + title="Histogram bins.", + description=""" +Defines the number of equal-width bins in the given range (10, by default). + +If bins is a sequence (comma `,` delimited values), it defines a monotonically increasing array of bin edges, including the rightmost edge, allowing for non-uniform bin widths. + +link: https://numpy.org/doc/stable/reference/generated/numpy.histogram.html + """, + openapi_examples={ + "user-provided": {"value": None}, + "simple": { + "description": "Defines the number of equal-width bins", + "value": 8, + }, + "array": { + "description": "Defines custom bin edges (comma `,` delimited values)", + "value": "0,100,200,300", + }, + }, + ), + ] = None + + range: Annotated[ + Optional[str], + Query( + alias="histogram_range", + title="Histogram range", + description=""" +Comma `,` delimited range of the bins. + +The lower and upper range of the bins. If not provided, range is simply (a.min(), a.max()). + +Values outside the range are ignored. The first element of the range must be less than or equal to the second. +range affects the automatic bin computation as well. + +link: https://numpy.org/doc/stable/reference/generated/numpy.histogram.html + """, + examples="0,1000", + ), + ] = None + + def __post_init__(self): + """Post Init.""" + if self.bins: + bins = self.bins.split(",") + if len(bins) == 1: + self.bins = int(bins[0]) # type: ignore + else: + self.bins = list(map(float, bins)) # type: ignore + else: + self.bins = 10 + + if self.range: + self.range = list(map(float, self.range.split(","))) # type: ignore ``` -### TilerFactory +
+ +#### `ImageRenderingParams` -The `TilerFactory` inherits dependency from `BaseTilerFactory`. +Control output image rendering options. -#### metadata_dependency +| Name | Type | Required | Default +| ------ | ---------- |----------|-------------- +| **rescale** | Query (str, comma delimited Numer) | No | None +| **color_formula** | Query (str) | No | None +| **return_mask** | Query (bool) | No | False -`rio_tiler.io.BaseReader.metadata()` methods options. +
```python @dataclass -class MetadataParams(DefaultDependency): - """Common Metadada parameters.""" +class ImageRenderingParams(DefaultDependency): + """Image Rendering options.""" - # Required params - pmin: float = Query(2.0, description="Minimum percentile") - pmax: float = Query(98.0, description="Maximum percentile") + rescale: Annotated[ + Optional[List[str]], + Query( + title="Min/Max data Rescaling", + description="comma (',') delimited Min,Max range. Can set multiple time for multiple bands.", + examples=["0,2000", "0,1000", "0,10000"], # band 1 # band 2 # band 3 + ), + ] = None + + color_formula: Annotated[ + Optional[str], + Query( + title="Color Formula", + description="rio-color formula (info: https://github.com/mapbox/rio-color)", + ), + ] = None + + add_mask: Annotated[ + Optional[bool], + Query( + alias="return_mask", + description="Add mask to the output data. Defaults to `True`", + ), + ] = None - # Optional params - max_size: Optional[int] = Query( - None, description="Maximum image size to read onto." - ) - histogram_bins: Optional[int] = Query(None, description="Histogram bins.") - histogram_range: Optional[str] = Query( - None, description="comma (',') delimited Min,Max histogram bounds" + def __post_init__(self): + """Post Init.""" + if self.rescale: + rescale_array = [] + for r in self.rescale: + parsed = tuple( + map( + float, + r.replace(" ", "").replace("[", "").replace("]", "").split(","), + ) + ) + assert ( + len(parsed) == 2 + ), f"Invalid rescale values: {self.rescale}, should be of form ['min,max', 'min,max'] or [[min,max], [min, max]]" + rescale_array.append(parsed) + + self.rescale: RescaleType = rescale_array # Noqa + +``` + +
+ +#### PartFeatureParams + +Same as `PreviewParams` but without default `max_size`. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **max_size** | Query (int) | No | None +| **height** | Query (int) | No | None +| **width** | Query (int) | No | None + +
+ +```python +@dataclass +class PreviewParams(DefaultDependency): + """Common Preview parameters.""" + + # NOTE: sizes dependency can either be a Query or a Path Parameter + max_size: Annotated[int, Field(description="Maximum image size to read onto.")] = ( + 1024 ) - bounds: Optional[str] = Query( - None, - descriptions="comma (',') delimited Bounding box coordinates from which to calculate image statistics.", + height: Annotated[ + Optional[int], Field(description="Force output image height.") + ] = None + width: Annotated[Optional[int], Field(description="Force output image width.")] = ( + None ) def __post_init__(self): """Post Init.""" - if self.max_size is not None: - self.kwargs["max_size"] = self.max_size - - if self.bounds: - self.kwargs["bounds"] = tuple(map(float, self.bounds.split(","))) - - hist_options = {} - if self.histogram_bins: - hist_options.update(dict(bins=self.histogram_bins)) - if self.histogram_range: - hist_options.update( - dict(range=list(map(float, self.histogram_range.split(",")))) - ) - if hist_options: - self.kwargs["hist_options"] = hist_options + if self.width or self.height: + self.max_size = None +``` + +
+ +#### PixelSelectionParams + +In `titiler.mosaic`, define pixel-selection method to apply. + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **pixel_selection** | Query (str) | No | 'first' + + +
+ +```python +def PixelSelectionParams( + pixel_selection: Annotated[ # type: ignore + Literal[tuple([e.name for e in PixelSelectionMethod])], + Query(description="Pixel selection method."), + ] = "first", +) -> MosaicMethodBase: + """ + Returns the mosaic method used to combine datasets together. + """ + return PixelSelectionMethod[pixel_selection].value() ``` -#### img_dependency +
+ +#### PreviewParams + +Define image output size. -Used in Crop/Preview to define size of the output image. +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **max_size** | Query (int) | No | 1024 +| **height** | Query (int) | No | None +| **width** | Query (int) | No | None + +
```python @dataclass -class ImageParams(DefaultDependency): - """Common Preview/Crop parameters.""" +class PreviewParams(DefaultDependency): + """Common Preview parameters.""" - max_size: Optional[int] = Query( - 1024, description="Maximum image size to read onto." - ) - height: Optional[int] = Query(None, description="Force output image height.") - width: Optional[int] = Query(None, description="Force output image width.") + max_size: Annotated[int, "Maximum image size to read onto."] = 1024 + height: Annotated[Optional[int], "Force output image height."] = None + width: Annotated[Optional[int], "Force output image width."] = None def __post_init__(self): """Post Init.""" @@ -294,70 +846,338 @@ class ImageParams(DefaultDependency): self.max_size = None ``` -### MultiBaseTilerFactory +
-The `MultiBaseTilerFactory` inherits dependency from `TilerFactory` and `BaseTilerFactory`. +#### StatisticsParams -#### assets_dependency +Define options for *rio-tiler*'s statistics method. -Define `assets`. +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **categorical** | Query (bool) | No | False +| **categories** | Query (list of Number) | No | None +| **p** | Query (list of Number) | No | [2, 98] + +
```python @dataclass -class AssetsParams(DefaultDependency): - """Assets parameters.""" +class StatisticsParams(DefaultDependency): + """Statistics options.""" + + categorical: Annotated[ + Optional[bool], + Query(description="Return statistics for categorical dataset. Defaults to `False` in rio-tiler"), + ] = None + categories: Annotated[ + Optional[List[Union[float, int]]], + Query( + alias="c", + title="Pixels values for categories.", + description="List of values for which to report counts.", + examples=[1, 2, 3], + ), + ] = None + percentiles: Annotated[ + Optional[List[int]], + Query( + alias="p", + title="Percentile values", + description="List of percentile values (default to [2, 98]).", + examples=[2, 5, 95, 98], + ), + ] = None - assets: List[str] = Query( - None, - title="Asset names", - description="Asset's names.", - examples={ - "one-asset": { - "description": "Return results for asset `data`.", - "value": ["data"], - }, - "multi-assets": { - "description": "Return results for assets `data` and `cog`.", - "value": ["data", "cog"], - }, - }, - ) + def __post_init__(self): + """Set percentiles default.""" + if not self.percentiles: + self.percentiles = [2, 98] ``` -### MultiBandTilerFactory +
-The `MultiBaseTilerFactory` inherits dependency from `TilerFactory` and `BaseTilerFactory`. +#### TileParams -#### bands_dependency +Define `buffer` and `padding` to apply at tile creation. -Define `bands`. +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **buffer** | Query (float) | No | None +| **padding** | Query (int) | No | None + +
```python @dataclass -class BandsParams(DefaultDependency): - """Band names parameters.""" +class TileParams(DefaultDependency): + """Tile options.""" + + buffer: Annotated[ + Optional[float], + Query( + gt=0, + title="Tile buffer.", + description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", + ), + ] = None + + padding: Annotated[ + Optional[int], + Query( + gt=0, + title="Tile padding.", + description="Padding to apply to each tile edge. Helps reduce resampling artefacts along edges. Defaults to `0`.", + ), + ] = None +``` - bands: List[str] = Query( - None, - title="Band names", - description="Band's names.", - examples={ - "one-band": { - "description": "Return results for band `B01`.", - "value": ["B01"], - }, - "multi-bands": { - "description": "Return results for bands `B01` and `B02`.", - "value": ["B01", "B02"], - }, - }, - ) +
+ +#### `algorithm.dependency` + +Control which `algorithm` to apply to the data. + +See [titiler.core.algorithm](https://github.com/developmentseed/titiler/blob/e46c35c8927b207f08443a274544901eb9ef3914/src/titiler/core/titiler/core/algorithm/__init__.py#L54-L79). + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **algorithm** | Query (str) | No | None +| **algorithm_params** | Query (encoded json) | No | None + +
+ +```python +algorithms = {} + +def post_process( + algorithm: Annotated[ + Literal[tuple(algorithms.keys())], + Query(description="Algorithm name"), + ] = None, + algorithm_params: Annotated[ + Optional[str], + Query(description="Algorithm parameter"), + ] = None, +) -> Optional[BaseAlgorithm]: + """Data Post-Processing options.""" + kwargs = json.loads(algorithm_params) if algorithm_params else {} + if algorithm: + try: + return algorithms.get(algorithm)(**kwargs) + + except ValidationError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + return None +``` + +
+ +## titiler.xarray + + +#### XarrayIOParams + +Define Xarray's `open_args` to `xarray.open_dataset`. + +| Name | Type | Required | Default +| ------ | ---------- |----------|-------------- +| **group** | Query (str) | No | None +| **decode_times** | Query (bool)| No | None + +
+ +```python +@dataclass +class XarrayIOParams(DefaultDependency): + """Dataset IO Options.""" + + group: Annotated[ + Optional[str], + Query( + description="Select a specific zarr group from a zarr hierarchy. Could be associated with a zoom level or dataset." + ), + ] = None + + decode_times: Annotated[ + Optional[bool], + Query( + title="decode_times", + description="Whether to decode times", + ), + ] = None +``` + +
+ +#### XarrayDsParams + +Define options to select a **variable** within a Xarray Dataset. + +| Name | Type | Required | Default +| ------ | ---------- |----------|-------------- +| **variable** | Query (str) | Yes | None +| **sel** | Query (list of str) | No | None +| **method** | Query (str)| No | None + +
+ +```python +@dataclass +class XarrayDsParams(DefaultDependency): + """Xarray Dataset Options.""" + + variable: Annotated[str, Query(description="Xarray Variable name.")] + + sel: Annotated[ + Optional[List[SelDimStr]], + Query( + description="Xarray Indexing using dimension names `{dimension}={value}`.", + ), + ] = None + + method: Annotated[ + Optional[Literal["nearest", "pad", "ffill", "backfill", "bfill"]], + Query( + alias="sel_method", + description="Xarray indexing method to use for inexact matches.", + ), + ] = None +``` + +
+ + +#### XarrayParams + +Combination of `XarrayIOParams` and `XarrayDsParams` + +| Name | Type | Required | Default +| ------ | ---------- |----------|-------------- +| **group** | Query (str) | No | None +| **decode_times** | Query (bool)| No | None +| **variable** | Query (str) | Yes | None +| **sel** | Query (list of str) | No | None +| **method** | Query (str)| No | None + +
+ +```python +@dataclass +class XarrayParams(XarrayIOParams, XarrayDsParams): + """Xarray Reader dependency.""" + + pass +``` + +
+ +#### CompatXarrayParams + +same as `XarrayParams` but with optional `variable` option. + +| Name | Type | Required | Default +| ------ | ---------- |----------|-------------- +| **group** | Query (str) | No | None +| **decode_times** | Query (bool)| No | None +| **variable** | Query (str) | No | None +| **sel** | Query (list of str) | No | None +| **method** | Query (str)| No | None + +
+ +```python +@dataclass +class XarrayParams(XarrayIOParams, XarrayDsParams): + """Xarray Reader dependency.""" + + pass ``` -### MosaicTilerFactory +
+ + +#### DatasetParams -The `MultiBaseTilerFactory` inherits dependency from `BaseTilerFactory`. +Same as `titiler.core.dependencies.DatasetParams` but with only `nodata` and `reproject` -#### backend_dependency +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **nodata** | Query (str, int, float) | No | None +| **reproject** | Query (str) | No | 'nearest' + +
+ +```python +@dataclass +class DatasetParams(DefaultDependency): + """Low level WarpedVRT Optional parameters.""" + + nodata: Annotated[ + Optional[Union[str, int, float]], + Query( + title="Nodata value", + description="Overwrite internal Nodata value", + ), + ] = None + reproject_method: Annotated[ + Optional[WarpResampling], + Query( + alias="reproject", + description="WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`.", + ), + ] = None + + def __post_init__(self): + """Post Init.""" + if self.nodata is not None: + self.nodata = numpy.nan if self.nodata == "nan" else float(self.nodata) +``` + +
+ + +#### PartFeatureParams + +Same as `titiler.core.dependencies.PartFeatureParams` but with `resampling` option + +| Name | Type | Required | Default +| ------ | ----------|----------|-------------- +| **max_size** | Query (int) | No | None +| **height** | Query (int) | No | None +| **width** | Query (int) | No | None +| **resampling** | Query (str) | No | 'nearest' + + +
+ +```python +@dataclass +class PartFeatureParams(DefaultDependency): + """Common parameters for bbox and feature.""" + + # NOTE: the part sizes dependency can either be a Query or a Path Parameter + max_size: Annotated[ + Optional[int], Field(description="Maximum image size to read onto.") + ] = None + height: Annotated[ + Optional[int], Field(description="Force output image height.") + ] = None + width: Annotated[Optional[int], Field(description="Force output image width.")] = ( + None + ) + resampling_method: Annotated[ + Optional[RIOResampling], + Query( + alias="resampling", + description="RasterIO resampling algorithm. Defaults to `nearest`.", + ), + ] = None + + def __post_init__(self): + """Post Init.""" + if self.width or self.height: + self.max_size = None +``` -Additional backend options. Defaults to `DefaultDependency` (empty). +
diff --git a/docs/src/advanced/endpoints_factories.md b/docs/src/advanced/endpoints_factories.md new file mode 100644 index 000000000..5aa66bed8 --- /dev/null +++ b/docs/src/advanced/endpoints_factories.md @@ -0,0 +1,428 @@ + +TiTiler's endpoints factories are helper functions that let users create a FastAPI *router* (`fastapi.APIRouter`) with a minimal set of endpoints. + +!!! Important + + Most of `tiler` **Factories** are built around [`rio_tiler.io.BaseReader`](https://cogeotiff.github.io/rio-tiler/advanced/custom_readers/), which defines basic methods to access datasets (e.g COG or STAC). The default reader is `Reader` for `TilerFactory` and `MosaicBackend` for `MosaicTilerFactory`. + + Factories classes use [dependencies injection](dependencies.md) to define most of the endpoint options. + +## titiler.core + +### BaseFactory + +class: `titiler.core.factory.BaseFactory` + +Most **Factories** are built from this [abstract based class](https://docs.python.org/3/library/abc.html) which is used to define commons attributes and utility functions shared between all factories. + +#### Attributes + +- **router**: FastAPI router. Defaults to `fastapi.APIRouter`. +- **router_prefix**: Set prefix to all factory's endpoint. Defaults to `""`. +- **route_dependencies**: Additional routes dependencies to add after routes creations. Defaults to `[]`. +- **extension**: TiTiler extensions to register after endpoints creations. Defaults to `[]`. +- **name**: Name of the Endpoints group. Defaults to `None`. +- **operation_prefix** (*private*): Endpoint's `operationId` prefix. Defined by `self.name` or `self.router_prefix.replace("/", ".")`. +- **conforms_to**: Set of conformance classes the Factory implement + +#### Methods + +- **register_routes**: Abstract method which needs to be define by each factories. +- **url_for**: Method to construct endpoint URL +- **add_route_dependencies**: Add dependencies to routes. + +### TilerFactory + +class: `titiler.core.factory.TilerFactory` + +Factory meant to create endpoints for single dataset using [*rio-tiler*'s `Reader`](https://cogeotiff.github.io/rio-tiler/readers/#rio_tileriorasterioreader). + +#### Attributes + +- **reader**: Dataset Reader **required**. +- **reader_dependency**: Dependency to control options passed to the reader instance init. Defaults to `titiler.core.dependencies.DefaultDependency` +- **path_dependency**: Dependency to use to define the dataset url. Defaults to `titiler.core.dependencies.DatasetPathParams`. +- **layer_dependency**: Dependency to define band indexes or expression. Defaults to `titiler.core.dependencies.BidxExprParams`. +- **dataset_dependency**: Dependency to overwrite `nodata` value, apply `rescaling` and change the `I/O` or `Warp` resamplings. Defaults to `titiler.core.dependencies.DatasetParams`. +- **tile_dependency**: Dependency to define `buffer` and `padding` to apply at tile creation. Defaults to `titiler.core.dependencies.TileParams`. +- **stats_dependency**: Dependency to define options for *rio-tiler*'s statistics method used in `/statistics` endpoints. Defaults to `titiler.core.dependencies.StatisticsParams`. +- **histogram_dependency**: Dependency to define *numpy*'s histogram options used in `/statistics` endpoints. Defaults to `titiler.core.dependencies.HistogramParams`. +- **img_preview_dependency**: Dependency to define image size for `/preview` and `/statistics` endpoints. Defaults to `titiler.core.dependencies.PreviewParams`. +- **img_part_dependency**: Dependency to define image size for `/bbox` and `/feature` endpoints. Defaults to `titiler.core.dependencies.PartFeatureParams`. +- **process_dependency**: Dependency to control which `algorithm` to apply to the data. Defaults to `titiler.core.algorithm.algorithms.dependency`. +- **colormap_dependency**: Dependency to define the Colormap options. Defaults to `titiler.core.dependencies.ColorMapParams` +- **render_dependency**: Dependency to control output image rendering options. Defaults to `titiler.core.dependencies.ImageRenderingParams` +- **environment_dependency**: Dependency to define GDAL environment at runtime. Default to `lambda: {}`. +- **supported_tms**: List of available TileMatrixSets. Defaults to `morecantile.tms`. +- **templates**: *Jinja2* templates to use in endpoints. Defaults to `titiler.core.factory.DEFAULT_TEMPLATES`. +- **render_func**: Image rendering method. Defaults to `titiler.core.utils.render_image`. +- **add_preview**: . Add `/preview` endpoint to the router. Defaults to `True`. +- **add_part**: . Add `/bbox` and `/feature` endpoints to the router. Defaults to `True`. +- **add_viewer**: . Add `/map.html` endpoints to the router. Defaults to `True`. + +#### Endpoints + +```python +from fastapi import FastAPI + +from titiler.core.factory import TilerFactory + +# Create FastAPI application +app = FastAPI() + +# Create router and register set of endpoints +cog = TilerFactory( + add_preview=True, + add_part=True, + add_viewer=True, +) + +# add router endpoint to the main application +app.include_router(cog.router) +``` + +| Method | URL | Output | Description +| ------ | --------------------------------------------------------------- |-------------------------------------------- |-------------- +| `GET` | `/bounds` | JSON ([Bounds][bounds_model]) | return dataset's bounds +| `GET` | `/info` | JSON ([Info][info_model]) | return dataset's basic info +| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][info_geojson_model]) | return dataset's basic info as a GeoJSON feature +| `GET` | `/statistics` | JSON ([Statistics][stats_model]) | return dataset's statistics +| `POST` | `/statistics` | GeoJSON ([Statistics][stats_geojson_model]) | return dataset's statistics for a GeoJSON +| `GET` | `/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/tiles/{tileMatrixSetId}/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset +| `GET` | `/{tileMatrixSetId}/map.html` | HTML | return a simple map viewer **Optional** +| `GET` | `/{tileMatrixSetId}/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document +| `GET` | `/{tileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities +| `GET` | `/point/{lon},{lat}` | JSON ([Point][point_model]) | return pixel values from a dataset +| `GET` | `/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset **Optional** +| `POST` | `/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a GeoJSON feature **Optional** +| `GET` | `/preview[/{width}x{height}][.{format}]` | image/bin | create a preview image from a dataset **Optional** + + +### MultiBaseTilerFactory + +class: `titiler.core.factory.MultiBaseTilerFactory` + +Custom `TilerFactory` to be used with [`rio_tiler.io.MultiBaseReader`](https://cogeotiff.github.io/rio-tiler/advanced/custom_readers/#multibasereader) type readers (e.g [`rio_tiler.io.STACReader`](https://cogeotiff.github.io/rio-tiler/readers/#rio_tileriostacstacreader)). + +#### Attributes + +- **reader**: `rio_tiler.io.base.MultiBaseReader` Dataset Reader **required**. +- **layer_dependency**: Dependency to define assets or expression. Defaults to `titiler.core.dependencies.AssetsBidxExprParams`. +- **assets_dependency**: Dependency to define assets to be used. Defaults to `titiler.core.dependencies.AssetsParams`. + +#### Endpoints + +```python +from fastapi import FastAPI + +from rio_tiler.io import STACReader # STACReader is a MultiBaseReader + +from titiler.core.factory import MultiBaseTilerFactory + +app = FastAPI() +stac = MultiBaseTilerFactory(reader=STACReader) +app.include_router(stac.router) +``` + +| Method | URL | Output | Description +| ------ | --------------------------------------------------------------- |------------------------------------------------- |-------------- +| `GET` | `/bounds` | JSON ([Bounds][bounds_model]) | return dataset's bounds +| `GET` | `/assets` | JSON | return the list of available assets +| `GET` | `/info` | JSON ([Info][multiinfo_model]) | return assets basic info +| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][multiinfo_geojson_model]) | return assets basic info as a GeoJSON feature +| `GET` | `/asset_statistics` | JSON ([Statistics][multistats_model]) | return per asset statistics +| `GET` | `/statistics` | JSON ([Statistics][stats_model]) | return assets statistics (merged) +| `POST` | `/statistics` | GeoJSON ([Statistics][multistats_geojson_model]) | return assets statistics for a GeoJSON (merged) +| `GET` | `/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/tiles/{tileMatrixSetId}/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from assets +| `GET` | `/{tileMatrixSetId}/map.html` | HTML | return a simple map viewer **Optional** +| `GET` | `/{tileMatrixSetId}/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document +| `GET` | `/{tileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities +| `GET` | `/point/{lon},{lat}` | JSON ([Point][multipoint_model]) | return pixel values from assets +| `GET` | `/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of assets **Optional** +| `POST` | `/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature intersecting assets **Optional** +| `GET` | `/preview[/{width}x{height}][.{format}]` | image/bin | create a preview image from assets **Optional** + +### MultiBandTilerFactory + +class: `titiler.core.factory.MultiBandTilerFactory` + +Custom `TilerFactory` to be used with [`rio_tiler.io.MultiBandReader`](https://cogeotiff.github.io/rio-tiler/advanced/custom_readers/#multibandsreader) type readers. + +#### Attributes + +- **reader**: `rio_tiler.io.base.MultiBandReader` Dataset Reader **required**. +- **layer_dependency**: Dependency to define assets or expression. Defaults to `titiler.core.dependencies.BandsExprParams`. +- **bands_dependency**: Dependency to define bands to be used. Defaults to `titiler.core.dependencies.BandsParams`. + +#### Endpoints + +```python +from fastapi import FastAPI, Query + + +from rio_tiler_pds.landsat.aws import LandsatC2Reader # LandsatC2Reader is a MultiBandReader +from titiler.core.factory import MultiBandTilerFactory + + +def SceneIDParams( + sceneid: Annotated[ + str, + Query(description="Landsat Scene ID") + ] +) -> str: + """Use `sceneid` in query instead of url.""" + return sceneid + + +app = FastAPI() +landsat = MultiBandTilerFactory(reader=LandsatC2Reader, path_dependency=SceneIDParams) +app.include_router(landsat.router) +``` + +| Method | URL | Output | Description +| ------ | --------------------------------------------------------------- |--------------------------------------------- |-------------- +| `GET` | `/bounds` | JSON ([Bounds][bounds_model]) | return dataset's bounds +| `GET` | `/bands` | JSON | return the list of available bands +| `GET` | `/info` | JSON ([Info][info_model]) | return basic info for a dataset +| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][info_geojson_model]) | return basic info for a dataset as a GeoJSON feature +| `GET` | `/statistics` | JSON ([Statistics][stats_model]) | return info and statistics for a dataset +| `POST` | `/statistics` | GeoJSON ([Statistics][stats_geojson_model]) | return info and statistics for a dataset +| `GET` | `/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/tiles/{tileMatrixSetId}/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset +| `GET` | `/{tileMatrixSetId}/map.html` | HTML | return a simple map viewer **Optional** +| `GET` | `/{tileMatrixSetId}/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document +| `GET` | `/{tileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities +| `GET` | `/point/{lon},{lat}` | JSON ([Point][point_model]) | return pixel value from a dataset +| `GET` | `/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset **Optional** +| `POST` | `/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature **Optional** +| `GET` | `/preview[/{width}x{height}][.{format}]` | image/bin | create a preview image from a dataset **Optional** + + +### TMSFactory + +class: `titiler.core.factory.TMSFactory` + +Endpoints factory for OGC `TileMatrixSets`. + +#### Attributes + +- **supported_tms**: List of available TileMatrixSets. Defaults to `morecantile.tms`. + +```python +from fastapi import FastAPI + +from titiler.core.factory import TMSFactory + +app = FastAPI() +tms = TMSFactory() +app.include_router(tms.router) +``` + +#### Endpoints + +| Method | URL | Output | Description +| ------ | ------------------------------------- |----------------------------------------------- |-------------- +| `GET` | `/tileMatrixSets` | JSON ([TileMatrixSetList][tilematrixset_list]) | retrieve the list of available tiling schemes (tile matrix sets) +| `GET` | `/tileMatrixSets/{tileMatrixSetId}` | JSON ([TileMatrixSet][tilematrixset]) | retrieve the definition of the specified tiling scheme (tile matrix set) + + +### AlgorithmFactory + +class: `titiler.core.factory.AlgorithmFactory` + +Endpoints factory for custom algorithms. + +#### Attributes + +- **supported_algorithm**: List of available `Algorithm`. Defaults to `titiler.core.algorithm.algorithms`. + +```python +from fastapi import FastAPI + +from titiler.core.factory import AlgorithmFactory + +app = FastAPI() +algo = AlgorithmFactory() +app.include_router(algo.router) +``` + +#### Endpoints + +| Method | URL | Output | Description +| ------ | ---------------------------- |--------------------------------------------------------- |-------------- +| `GET` | `/algorithms` | JSON (Dict of [Algorithm Metadata][algorithm_metadata]) | retrieve the list of available Algorithms +| `GET` | `/algorithms/{algorithmId}` | JSON ([Algorithm Metadata][algorithm_metadata]) | retrieve the metadata of the specified algorithm. + + +### ColorMapFactory + +class: `titiler.core.factory.ColorMapFactory` + +Endpoints factory for colorMaps metadata. + +#### Attributes + +- **supported_colormaps**: List of available `ColorMaps`. Defaults to `rio_tiler.colormap.cmap`. + +```python +from fastapi import FastAPI + +from titiler.core.factory import ColorMapFactory + +app = FastAPI() +colormap = ColorMapFactory() +app.include_router(colormap.router) +``` + +#### Endpoints + +| Method | URL | Output | Description +| ------ | ---------------------------- |-------------------------------------- |-------------- +| `GET` | `/colorMaps` | JSON ([colorMapList][colormap_list]) | retrieve the list of available colorMaps +| `GET` | `/colorMaps/{colorMapId}` | JSON ([colorMap][colormap]) | retrieve the metadata or image of the specified colorMap. + + +## titiler.mosaic + +### MosaicTilerFactory + +class: `titiler.mosaic.factory.MosaicTilerFactory` + +Endpoints factory for mosaics, built on top of [MosaicJSON](https://github.com/developmentseed/mosaicjson-spec). + +#### Attributes + +- **backend**: `cogeo_mosaic.backends.BaseBackend` Mosaic backend. Defaults to `cogeo_mosaic.backend.MosaicBackend`. +- **backend_dependency**: Dependency to control options passed to the backend instance init. Defaults to `titiler.core.dependencies.DefaultDependency` +- **dataset_reader**: Dataset Reader. Defaults to `rio_tiler.io.Reader` +- **reader_dependency**: Dependency to control options passed to the reader instance init. Defaults to `titiler.core.dependencies.DefaultDependency` +- **path_dependency**: Dependency to use to define the dataset url. Defaults to `titiler.mosaic.factory.DatasetPathParams`. +- **assets_accessor_dependency**: Dependency to define options to be forwarded to the backend `get_assets` method. Defaults to `titiler.core.dependencies.DefaultDependency`. +- **layer_dependency**: Dependency to define band indexes or expression. Defaults to `titiler.core.dependencies.BidxExprParams`. +- **dataset_dependency**: Dependency to overwrite `nodata` value, apply `rescaling` and change the `I/O` or `Warp` resamplings. Defaults to `titiler.core.dependencies.DatasetParams`. +- **tile_dependency**: Dependency to define `buffer` and `padding` to apply at tile creation. Defaults to `titiler.core.dependencies.TileParams`. +- **process_dependency**: Dependency to control which `algorithm` to apply to the data. Defaults to `titiler.core.algorithm.algorithms.dependency`. +- **colormap_dependency**: Dependency to define the Colormap options. Defaults to `titiler.core.dependencies.ColorMapParams` +- **render_dependency**: Dependency to control output image rendering options. Defaults to `titiler.core.dependencies.ImageRenderingParams` +- **pixel_selection_dependency**: Dependency to select the `pixel_selection` method. Defaults to `titiler.mosaic.factory.PixelSelectionParams`. +- **environment_dependency**: Dependency to define GDAL environment at runtime. Default to `lambda: {}`. +- **supported_tms**: List of available TileMatrixSets. Defaults to `morecantile.tms`. +- **supported_tms**: List of available TileMatrixSets. Defaults to `morecantile.tms`. +- **templates**: *Jinja2* templates to use in endpoints. Defaults to `titiler.core.factory.DEFAULT_TEMPLATES`. +- **optional_headers**: List of OptionalHeader which endpoints could add (if implemented). Defaults to `[]`. +- **add_viewer**: . Add `/map.html` endpoints to the router. Defaults to `True`. + +#### Endpoints + +| Method | URL | Output | Description +| ------ | --------------------------------------------------------------- |--------------------------------------------------- |-------------- +| `GET` | `/` | JSON [MosaicJSON][mosaic_model] | return a MosaicJSON document +| `GET` | `/bounds` | JSON ([Bounds][bounds_model]) | return mosaic's bounds +| `GET` | `/info` | JSON ([Info][mosaic_info_model]) | return mosaic's basic info +| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][mosaic_geojson_info_model]) | return mosaic's basic info as a GeoJSON feature +| `GET` | `/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/tiles/{tileMatrixSetId}/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a MosaicJSON +| `GET` | `/tiles/{tileMatrixSetId}/{z}/{x}/{y}/assets` | JSON | return list of assets intersecting a XYZ tile +| `GET` | `/{tileMatrixSetId}/map.html` | HTML | return a simple map viewer **Optional** +| `GET` | `/{tileMatrixSetId}/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document +| `GET` | `/{tileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities +| `GET` | `/point/{lon},{lat}` | JSON ([Point][mosaic_point]) | return pixel value from a MosaicJSON dataset +| `GET` | `/point/{lon},{lat}/assets` | JSON | return list of assets intersecting a point +| `GET` | `/bbox/{minx},{miny},{maxx},{maxy}/assets` | JSON | return list of assets intersecting a bounding box + +## titiler.xarray + +### TilerFactory + +class: `titiler.xarray.factory.TilerFactory` + +#### Attributes + +- **reader**: Dataset Reader **required**. +- **path_dependency**: Dependency to use to define the dataset url. Defaults to `titiler.core.dependencies.DatasetPathParams`. +- **reader_dependency**: Dependency to control options passed to the reader instance init. Defaults to `titiler.xarray.dependencies.XarrayParams` +- **layer_dependency**: Dependency to define band indexes or expression. Defaults to `titiler.core.dependencies.BidxParams`. +- **dataset_dependency**: Dependency to overwrite `nodata` value and change the `Warp` resamplings. Defaults to `titiler.xarray.dependencies.DatasetParams`. +- **tile_dependency**: Dependency for tile creation options. Defaults to `titiler.core.dependencies.DefaultDependency`. +- **stats_dependency**: Dependency to define options for *rio-tiler*'s statistics method used in `/statistics` endpoints. Defaults to `titiler.core.dependencies.StatisticsParams`. +- **histogram_dependency**: Dependency to define *numpy*'s histogram options used in `/statistics` endpoints. Defaults to `titiler.core.dependencies.HistogramParams`. +- **img_part_dependency**: Dependency to define image size for `/bbox` and `/feature` endpoints. Defaults to `titiler.xarray.dependencies.PartFeatureParams`. +- **process_dependency**: Dependency to control which `algorithm` to apply to the data. Defaults to `titiler.core.algorithm.algorithms.dependency`. +- **colormap_dependency**: Dependency to define the Colormap options. Defaults to `titiler.core.dependencies.ColorMapParams` +- **render_dependency**: Dependency to control output image rendering options. Defaults to `titiler.core.dependencies.ImageRenderingParams` +- **environment_dependency**: Dependency to define GDAL environment at runtime. Default to `lambda: {}`. +- **supported_tms**: List of available TileMatrixSets. Defaults to `morecantile.tms`. +- **templates**: *Jinja2* templates to use in endpoints. Defaults to `titiler.core.factory.DEFAULT_TEMPLATES`. +- **add_part**: . Add `/bbox` and `/feature` endpoints to the router. Defaults to `True`. +- **add_viewer**: . Add `/map.html` endpoints to the router. Defaults to `True`. + + +```python +from fastapi import FastAPI + +from titiler.xarray.factory import TilerFactory + +# Create FastAPI application +app = FastAPI() + +# Create router and register set of endpoints +md = TilerFactory( + add_part=True, + add_viewer=True, +) + +# add router endpoint to the main application +app.include_router(md.router) +``` + +#### Endpoints + +| Method | URL | Output | Description +| ------ | --------------------------------------------------------------- |-------------------------------------------- |-------------- +| `GET` | `/bounds` | JSON ([Bounds][bounds_model]) | return dataset's bounds +| `GET` | `/info` | JSON ([Info][info_model]) | return dataset's basic info +| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][info_geojson_model]) | return dataset's basic info as a GeoJSON feature +| `POST` | `/statistics` | GeoJSON ([Statistics][stats_geojson_model]) | return dataset's statistics for a GeoJSON +| `GET` | `/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/tiles/{tileMatrixSetId}/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset +| `GET` | `/{tileMatrixSetId}/map.html` | HTML | return a simple map viewer **Optional** +| `GET` | `/{tileMatrixSetId}/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document +| `GET` | `/{tileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities +| `GET` | `/point/{lon},{lat}` | JSON ([Point][point_model]) | return pixel values from a dataset +| `GET` | `/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset **Optional** +| `POST` | `/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a GeoJSON feature **Optional** + + +[bounds_model]: https://github.com/cogeotiff/rio-tiler/blob/9aaa88000399ee8d36e71d176f67b6ea3ec53f2d/rio_tiler/models.py#L43-L46 +[info_model]: https://github.com/cogeotiff/rio-tiler/blob/9aaa88000399ee8d36e71d176f67b6ea3ec53f2d/rio_tiler/models.py#L56-L72 +[info_geojson_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L30 +[tilejson_model]: https://github.com/developmentseed/titiler/blob/2335048a407f17127099cbbc6c14e1328852d619/src/titiler/core/titiler/core/models/mapbox.py#L16-L38 +[point_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L11-L20 +[stats_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L32 +[stats_geojson_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L46-L49 + +[multiinfo_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L52 +[multiinfo_geojson_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L53 +[multipoint_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L23-L27 +[multistats_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L55 +[multistats_geojson_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L56-L59 + +[mosaic_info_model]: https://github.com/developmentseed/cogeo-mosaic/blob/1dc3c873472c8cf7634ad893b9cdc40105ca3874/cogeo_mosaic/models.py#L9-L17 +[mosaic_geojson_info_model]: https://github.com/developmentseed/titiler/blob/2bd1b159a9cf0932ad14e9eabf1e4e66498adbdc/src/titiler/mosaic/titiler/mosaic/factory.py#L130 +[mosaic_model]: https://github.com/developmentseed/cogeo-mosaic/blob/1dc3c873472c8cf7634ad893b9cdc40105ca3874/cogeo_mosaic/mosaic.py#L55-L72 +[mosaic_point]: https://github.com/developmentseed/titiler/blob/2bd1b159a9cf0932ad14e9eabf1e4e66498adbdc/src/titiler/mosaic/titiler/mosaic/models/responses.py#L8-L17 + +[tilematrixset_list]: https://github.com/developmentseed/titiler/blob/ffd67af34c2807a6e1447817f943446a58441ed8/src/titiler/core/titiler/core/models/OGC.py#L33-L40 +[tilematrixset]: https://github.com/developmentseed/morecantile/blob/eec54326ce2b134cfbc03dd69a3e2938e4109101/morecantile/models.py#L399-L490 + +[algorithm_metadata]: https://github.com/developmentseed/titiler/blob/ffd67af34c2807a6e1447817f943446a58441ed8/src/titiler/core/titiler/core/algorithm/base.py#L32-L40 + +[colormap_list]: https://github.com/developmentseed/titiler/blob/535304fd7e1b0bfbb791bdec8cbfb6e78b4a6eb5/src/titiler/core/titiler/core/models/responses.py#L51-L55 +[colormap]: https://github.com/cogeotiff/rio-tiler/blob/6343b571a367ef63a10d6807e3d907c3283ebb20/rio_tiler/types.py#L24-L27 diff --git a/docs/src/advanced/telemetry.md b/docs/src/advanced/telemetry.md new file mode 100644 index 000000000..a7de597d0 --- /dev/null +++ b/docs/src/advanced/telemetry.md @@ -0,0 +1,87 @@ + +## Observability with OpenTelemetry + +`TiTiler` provides built-in observability through OpenTelemetry, automatically creating traces for all API endpoints. These traces include detailed spans for key internal operations like data access and image processing, enabling fine-grained performance analysis and debugging. + +This instrumentation works seamlessly with other OpenTelemetry libraries, such as FastAPIInstrumentor, to provide a complete, end-to-end view of your application's performance, from incoming request to final response. + +### Installation + +To enable telemetry, you must install titiler.core with the [telemetry] extra. This ensures all necessary OpenTelemetry packages are installed. + +```bash +python -m pip install -U pip + +# From Pypi +python -m pip install titiler.core[telemetry] + +# Or from sources +git clone https://github.com/developmentseed/titiler.git +cd titiler && python -m pip install -e src/titiler/core[telemetry] +``` + +### Configuration + +To export traces, you need to configure your application to send them to an observability platform (like Jaeger or Datadog) using an OTLP Exporter. + +The following example demonstrates how to set up a tracer provider that exports data via the OTLP protocol over HTTP. This setup is typically done once when your application starts. + +```python +# In your main application file, e.g., main.py + +import os +from fastapi import FastAPI +from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor +from opentelemetry.instrumentation.logging import LoggingInstrumentor +from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_VERSION, Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor + +from titiler.core.factory import TilerFactory + +# --- OpenTelemetry Configuration --- + +# Define a "Resource" for your application. +# This adds metadata to your traces, like the service name and version. +resource = Resource.create( + { + SERVICE_NAME: os.getenv("OTEL_SERVICE_NAME", "titiler"), + SERVICE_VERSION: "0.1", + } +) + +# Create a "TracerProvider" with the defined resource. +# The provider manages the creation of tracers. +provider = TracerProvider(resource=resource) + +# Configure an "Exporter" to send telemetry data. +# The OTLPSpanExporter sends data to an OTLP-compatible endpoint. +# By default, it reads the endpoint from the OTEL_EXPORTER_OTLP_ENDPOINT +# environment variable. The default for HTTP is http://localhost:4318. +exporter = OTLPSpanExporter() + +# Use a "BatchSpanProcessor" to send spans in the background. +# This is the recommended processor for production. +processor = BatchSpanProcessor(exporter) +provider.add_span_processor(processor) + +# Set the configured provider as the global tracer provider. +trace.set_tracer_provider(provider) + +# --- FastAPI Application Setup --- +app = FastAPI(title="My TiTiler App") + +# Instrument the FastAPI application. +# This adds middleware to trace requests, responses, and exceptions, +# complementing TiTiler's internal endpoint tracing. +FastAPIInstrumentor.instrument_app(app) + +# Add trace/span info to logging messages for trace correlation +LoggingInstrumentor().instrument(set_logging_format=True) + +# Add your TiTiler endpoints with the enable_telemetry flag set to True +cog = TilerFactory(enable_telemetry=True) +app.include_router(cog.router) +``` diff --git a/docs/src/advanced/tiler_factories.md b/docs/src/advanced/tiler_factories.md deleted file mode 100644 index c97dadd15..000000000 --- a/docs/src/advanced/tiler_factories.md +++ /dev/null @@ -1,147 +0,0 @@ - -Tiler factories are helper functions that let users create a FastAPI router (`fastapi.APIRouter`) with a minimal set of endpoints. - -### `titiler.core.factory.TilerFactory` - -```python -from fastapi import FastAPI - -from titiler.core.factory import TilerFactory - -app = FastAPI(description="A lightweight Cloud Optimized GeoTIFF tile server") -cog = TilerFactory() -app.include_router(cog.router, tags=["Cloud Optimized GeoTIFF"]) -``` - -| Method | URL | Output | Description -| ------ | --------------------------------------------------------------- |-------------------------------------------- |-------------- -| `GET` | `/bounds` | JSON ([Bounds][bounds_model]) | return dataset's bounds -| `GET` | `/info` | JSON ([Info][info_model]) | return dataset's basic info -| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][info_geojson_model]) | return dataset's basic info as a GeoJSON feature -| `GET` | `/statistics` | JSON ([Statistics][stats_model]) | return dataset's statistics -| `POST` | `/statistics` | GeoJSON ([Statistics][stats_geojson_model]) | return dataset's statistics for a GeoJSON -| `GET` | `/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset -| `GET` | `[/{TileMatrixSetId}]/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document -| `GET` | `[/{TileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities -| `GET` | `/point/{lon},{lat}` | JSON ([Point][point_model]) | return pixel values from a dataset -| `GET` | `/preview[.{format}]` | image/bin | create a preview image from a dataset (**Optional**) -| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset (**Optional**) -| `POST` | `/crop[/{width}x{height}][.{format}]` | image/bin | create an image from a GeoJSON feature (**Optional**) -| `GET` | `/map` | HTML | return a simple map viewer -| `GET` | `[/{TileMatrixSetId}]/map` | HTML | return a simple map viewer - -### `titiler.core.factory.MultiBaseTilerFactory` - -Custom `TilerFactory` to be used with `rio_tiler.io.MultiBaseReader` type readers. - -```python -from fastapi import FastAPI -from rio_tiler.io import STACReader # rio_tiler.io.STACReader is a MultiBaseReader - -from titiler.core.factory import MultiBaseTilerFactory - -app = FastAPI(description="A lightweight STAC tile server") -cog = MultiBaseTilerFactory(reader=STACReader) -app.include_router(cog.router, tags=["STAC"]) -``` - -| Method | URL | Output | Description -| ------ | --------------------------------------------------------------- |------------------------------------------------- |-------------- -| `GET` | `/bounds` | JSON ([Bounds][bounds_model]) | return dataset's bounds -| `GET` | `/assets` | JSON | return the list of available assets -| `GET` | `/info` | JSON ([Info][multiinfo_model]) | return assets basic info -| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][multiinfo_geojson_model]) | return assets basic info as a GeoJSON feature -| `GET` | `/asset_statistics` | JSON ([Statistics][multistats_model]) | return per asset statistics -| `GET` | `/statistics` | JSON ([Statistics][stats_model]) | return assets statistics (merged) -| `POST` | `/statistics` | GeoJSON ([Statistics][multistats_geojson_model]) | return assets statistics for a GeoJSON (merged) -| `GET` | `/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from assets -| `GET` | `/[{TileMatrixSetId}]/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document -| `GET` | `/{TileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities -| `GET` | `/point/{lon},{lat}` | JSON ([Point][multipoint_model]) | return pixel values from assets -| `GET` | `/preview[.{format}]` | image/bin | create a preview image from assets (**Optional**) -| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of assets (**Optional**) -| `POST` | `/crop[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature intersecting assets (**Optional**) -| `GET` | `[/{TileMatrixSetId}]/map` | HTML | return a simple map viewer - -### `titiler.core.factory.MultiBandTilerFactory` - -Custom `TilerFactory` to be used with `rio_tiler.io.MultiBandReader` type readers. - -```python -from fastapi import FastAPI, Query -from rio_tiler_pds.landsat.aws import LandsatC2Reader # rio_tiler_pds.landsat.aws.LandsatC2Reader is a MultiBandReader - -from titiler.core.factory import MultiBandTilerFactory - - -def SceneIDParams(sceneid: str = Query(..., description="Landsat Scene ID")) -> str: - """Use `sceneid` in query instead of url.""" - return sceneid - - -app = FastAPI(description="A lightweight Landsat Collection 2 tile server") -cog = MultiBandTilerFactory(reader=LandsatC2Reader, path_dependency=SceneIDParams) -app.include_router(cog.router, tags=["Landsat"]) -``` - -| Method | URL | Output | Description -| ------ | --------------------------------------------------------------- |--------------------------------------------- |-------------- -| `GET` | `/bounds` | JSON ([Bounds][bounds_model]) | return dataset's bounds -| `GET` | `/bands` | JSON | return the list of available bands -| `GET` | `/info` | JSON ([Info][info_model]) | return basic info for a dataset -| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][info_geojson_model]) | return basic info for a dataset as a GeoJSON feature -| `GET` | `/statistics` | JSON ([Statistics][stats_model]) | return info and statistics for a dataset -| `POST` | `/statistics` | GeoJSON ([Statistics][stats_geojson_model]) | return info and statistics for a dataset -| `GET` | `/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset -| `GET` | `/[{TileMatrixSetId}]/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document -| `GET` | `/{TileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities -| `GET` | `/point/{lon},{lat}` | JSON ([Point][point_model]) | return pixel value from a dataset -| `GET` | `/preview[.{format}]` | image/bin | create a preview image from a dataset -| `GET` | `/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset -| `POST` | `/crop[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson feature -| `GET` | `[/{TileMatrixSetId}]/map` | HTML | return a simple map viewer - -### `titiler.mosaic.factory.MosaicTilerFactory` - - -| Method | URL | Output | Description -| ------ | --------------------------------------------------------------- |--------------------------------------------------- |-------------- -| `GET` | `/` | JSON [MosaicJSON][mosaic_model] | return a MosaicJSON document -| `GET` | `/bounds` | JSON ([Bounds][bounds_model]) | return mosaic's bounds -| `GET` | `/info` | JSON ([Info][mosaic_info_model]) | return mosaic's basic info -| `GET` | `/info.geojson` | GeoJSON ([InfoGeoJSON][mosaic_geojson_info_model]) | return mosaic's basic info as a GeoJSON feature -| `GET` | `/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a MosaicJSON -| `GET` | `[/{TileMatrixSetId}]/tilejson.json` | JSON ([TileJSON][tilejson_model]) | return a Mapbox TileJSON document -| `GET` | `[/{TileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities -| `GET` | `/point/{lon},{lat}` | JSON ([Point][mosaic_point]) | return pixel value from a MosaicJSON dataset -| `GET` | `/{z}/{x}/{y}/assets` | JSON | return list of assets intersecting a XYZ tile -| `GET` | `/{lon},{lat}/assets` | JSON | return list of assets intersecting a point -| `GET` | `/{minx},{miny},{maxx},{maxy}/assets` | JSON | return list of assets intersecting a bounding box -| `GET` | `[/{TileMatrixSetId}]/map` | HTML | return a simple map viewer - - -!!! Important - - **Factories** are built around [`rio_tiler.io.BaseReader`](https://cogeotiff.github.io/rio-tiler/advanced/custom_readers/), which defines basic methods to access datasets (e.g COG or STAC). The default reader is `COGReader` for `TilerFactory` and `MosaicBackend` for `MosaicTilerFactory`. - - Factories classes use [dependencies injection](dependencies.md) to define most of the endpoint options. - - -[bounds_model]: https://github.com/cogeotiff/rio-tiler/blob/9aaa88000399ee8d36e71d176f67b6ea3ec53f2d/rio_tiler/models.py#L43-L46 -[info_model]: https://github.com/cogeotiff/rio-tiler/blob/9aaa88000399ee8d36e71d176f67b6ea3ec53f2d/rio_tiler/models.py#L56-L72 -[info_geojson_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L30 -[tilejson_model]: https://github.com/developmentseed/titiler/blob/2335048a407f17127099cbbc6c14e1328852d619/src/titiler/core/titiler/core/models/mapbox.py#L16-L38 -[point_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L11-L20 -[stats_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L32 -[stats_geojson_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L46-L49 - -[multiinfo_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L52 -[multiinfo_geojson_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L53 -[multipoint_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L23-L27 -[multistats_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L55 -[multistats_geojson_model]: https://github.com/developmentseed/titiler/blob/c97e251c46b51703d41b1c9e66bc584649aa231c/src/titiler/core/titiler/core/models/responses.py#L56-L59 - -[mosaic_info_model]: https://github.com/developmentseed/cogeo-mosaic/blob/1dc3c873472c8cf7634ad893b9cdc40105ca3874/cogeo_mosaic/models.py#L9-L17 -[mosaic_geojson_info_model]: https://github.com/developmentseed/titiler/blob/2bd1b159a9cf0932ad14e9eabf1e4e66498adbdc/src/titiler/mosaic/titiler/mosaic/factory.py#L130 -[mosaic_model]: https://github.com/developmentseed/cogeo-mosaic/blob/1dc3c873472c8cf7634ad893b9cdc40105ca3874/cogeo_mosaic/mosaic.py#L55-L72 -[mosaic_point]: https://github.com/developmentseed/titiler/blob/2bd1b159a9cf0932ad14e9eabf1e4e66498adbdc/src/titiler/mosaic/titiler/mosaic/models/responses.py#L8-L17 diff --git a/docs/src/api/titiler/core/dependencies.md b/docs/src/api/titiler/core/dependencies.md index 51ae7afb2..3e4e260f6 100644 --- a/docs/src/api/titiler/core/dependencies.md +++ b/docs/src/api/titiler/core/dependencies.md @@ -1,1471 +1,5 @@ -# Module titiler.core.dependencies -Common dependency. +::: titiler.core.dependencies + options: + show_source: true -None - -## Functions - - -### ColorMapParams - -```python3 -def ColorMapParams( - colormap_name: titiler.core.dependencies.ColorMapName = Query(None), - colormap: str = Query(None) -) -> Union[Dict, Sequence, NoneType] -``` - - -Colormap Dependency. - - -### DatasetPathParams - -```python3 -def DatasetPathParams( - url: str = Query(Ellipsis) -) -> str -``` - - -Create dataset path from args - - -### TMSParams - -```python3 -def TMSParams( - TileMatrixSetId: titiler.core.dependencies.TileMatrixSetName = Query(TileMatrixSetName.WebMercatorQuad) -) -> morecantile.models.TileMatrixSet -``` - - -TileMatrixSet Dependency. - - -### WebMercatorTMSParams - -```python3 -def WebMercatorTMSParams( - TileMatrixSetId: titiler.core.dependencies.WebMercatorTileMatrixSetName = Query(WebMercatorTileMatrixSetName.WebMercatorQuad) -) -> morecantile.models.TileMatrixSet -``` - - -TileMatrixSet Dependency. - -## Classes - -### AssetsBidxExprParams - -```python3 -class AssetsBidxExprParams( - assets: Union[List[str], NoneType] = Query(None), - expression: Union[str, NoneType] = Query(None), - asset_indexes: Union[Sequence[str], NoneType] = Query(None), - asset_expression: Union[Sequence[str], NoneType] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.DefaultDependency - -#### Class variables - -```python3 -asset_expression -``` - -```python3 -asset_indexes -``` - -```python3 -assets -``` - -```python3 -expression -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### AssetsBidxParams - -```python3 -class AssetsBidxParams( - assets: List[str] = Query(None), - asset_indexes: Union[Sequence[str], NoneType] = Query(None), - asset_expression: Union[Sequence[str], NoneType] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.AssetsParams -* titiler.core.dependencies.DefaultDependency - -#### Class variables - -```python3 -asset_expression -``` - -```python3 -asset_indexes -``` - -```python3 -assets -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### AssetsParams - -```python3 -class AssetsParams( - assets: List[str] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.DefaultDependency - -#### Descendants - -* titiler.core.dependencies.AssetsBidxParams - -#### Class variables - -```python3 -assets -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### BandsExprParams - -```python3 -class BandsExprParams( - bands: List[str] = Query(None), - expression: Union[str, NoneType] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.ExpressionParams -* titiler.core.dependencies.BandsParams -* titiler.core.dependencies.DefaultDependency - -#### Class variables - -```python3 -bands -``` - -```python3 -expression -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### BandsExprParamsOptional - -```python3 -class BandsExprParamsOptional( - bands: List[str] = Query(None), - expression: Union[str, NoneType] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.ExpressionParams -* titiler.core.dependencies.BandsParams -* titiler.core.dependencies.DefaultDependency - -#### Class variables - -```python3 -bands -``` - -```python3 -expression -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### BandsParams - -```python3 -class BandsParams( - bands: List[str] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.DefaultDependency - -#### Descendants - -* titiler.core.dependencies.BandsExprParamsOptional -* titiler.core.dependencies.BandsExprParams - -#### Class variables - -```python3 -bands -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### BidxExprParams - -```python3 -class BidxExprParams( - indexes: Union[List[int], NoneType] = Query(None), - expression: Union[str, NoneType] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.ExpressionParams -* titiler.core.dependencies.BidxParams -* titiler.core.dependencies.DefaultDependency - -#### Class variables - -```python3 -expression -``` - -```python3 -indexes -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### BidxParams - -```python3 -class BidxParams( - indexes: Union[List[int], NoneType] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.DefaultDependency - -#### Descendants - -* titiler.core.dependencies.BidxExprParams - -#### Class variables - -```python3 -indexes -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### ColorMapName - -```python3 -class ColorMapName( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* enum.Enum - -#### Class variables - -```python3 -accent -``` - -```python3 -accent_r -``` - -```python3 -afmhot -``` - -```python3 -afmhot_r -``` - -```python3 -autumn -``` - -```python3 -autumn_r -``` - -```python3 -binary -``` - -```python3 -binary_r -``` - -```python3 -blues -``` - -```python3 -blues_r -``` - -```python3 -bone -``` - -```python3 -bone_r -``` - -```python3 -brbg -``` - -```python3 -brbg_r -``` - -```python3 -brg -``` - -```python3 -brg_r -``` - -```python3 -bugn -``` - -```python3 -bugn_r -``` - -```python3 -bupu -``` - -```python3 -bupu_r -``` - -```python3 -bwr -``` - -```python3 -bwr_r -``` - -```python3 -cfastie -``` - -```python3 -cividis -``` - -```python3 -cividis_r -``` - -```python3 -cmrmap -``` - -```python3 -cmrmap_r -``` - -```python3 -cool -``` - -```python3 -cool_r -``` - -```python3 -coolwarm -``` - -```python3 -coolwarm_r -``` - -```python3 -copper -``` - -```python3 -copper_r -``` - -```python3 -cubehelix -``` - -```python3 -cubehelix_r -``` - -```python3 -dark2 -``` - -```python3 -dark2_r -``` - -```python3 -flag -``` - -```python3 -flag_r -``` - -```python3 -gist_earth -``` - -```python3 -gist_earth_r -``` - -```python3 -gist_gray -``` - -```python3 -gist_gray_r -``` - -```python3 -gist_heat -``` - -```python3 -gist_heat_r -``` - -```python3 -gist_ncar -``` - -```python3 -gist_ncar_r -``` - -```python3 -gist_rainbow -``` - -```python3 -gist_rainbow_r -``` - -```python3 -gist_stern -``` - -```python3 -gist_stern_r -``` - -```python3 -gist_yarg -``` - -```python3 -gist_yarg_r -``` - -```python3 -gnbu -``` - -```python3 -gnbu_r -``` - -```python3 -gnuplot -``` - -```python3 -gnuplot2 -``` - -```python3 -gnuplot2_r -``` - -```python3 -gnuplot_r -``` - -```python3 -gray -``` - -```python3 -gray_r -``` - -```python3 -greens -``` - -```python3 -greens_r -``` - -```python3 -greys -``` - -```python3 -greys_r -``` - -```python3 -hot -``` - -```python3 -hot_r -``` - -```python3 -hsv -``` - -```python3 -hsv_r -``` - -```python3 -inferno -``` - -```python3 -inferno_r -``` - -```python3 -jet -``` - -```python3 -jet_r -``` - -```python3 -magma -``` - -```python3 -magma_r -``` - -```python3 -name -``` - -```python3 -nipy_spectral -``` - -```python3 -nipy_spectral_r -``` - -```python3 -ocean -``` - -```python3 -ocean_r -``` - -```python3 -oranges -``` - -```python3 -oranges_r -``` - -```python3 -orrd -``` - -```python3 -orrd_r -``` - -```python3 -paired -``` - -```python3 -paired_r -``` - -```python3 -pastel1 -``` - -```python3 -pastel1_r -``` - -```python3 -pastel2 -``` - -```python3 -pastel2_r -``` - -```python3 -pink -``` - -```python3 -pink_r -``` - -```python3 -piyg -``` - -```python3 -piyg_r -``` - -```python3 -plasma -``` - -```python3 -plasma_r -``` - -```python3 -prgn -``` - -```python3 -prgn_r -``` - -```python3 -prism -``` - -```python3 -prism_r -``` - -```python3 -pubu -``` - -```python3 -pubu_r -``` - -```python3 -pubugn -``` - -```python3 -pubugn_r -``` - -```python3 -puor -``` - -```python3 -puor_r -``` - -```python3 -purd -``` - -```python3 -purd_r -``` - -```python3 -purples -``` - -```python3 -purples_r -``` - -```python3 -rainbow -``` - -```python3 -rainbow_r -``` - -```python3 -rdbu -``` - -```python3 -rdbu_r -``` - -```python3 -rdgy -``` - -```python3 -rdgy_r -``` - -```python3 -rdpu -``` - -```python3 -rdpu_r -``` - -```python3 -rdylbu -``` - -```python3 -rdylbu_r -``` - -```python3 -rdylgn -``` - -```python3 -rdylgn_r -``` - -```python3 -reds -``` - -```python3 -reds_r -``` - -```python3 -rplumbo -``` - -```python3 -schwarzwald -``` - -```python3 -seismic -``` - -```python3 -seismic_r -``` - -```python3 -set1 -``` - -```python3 -set1_r -``` - -```python3 -set2 -``` - -```python3 -set2_r -``` - -```python3 -set3 -``` - -```python3 -set3_r -``` - -```python3 -spectral -``` - -```python3 -spectral_r -``` - -```python3 -spring -``` - -```python3 -spring_r -``` - -```python3 -summer -``` - -```python3 -summer_r -``` - -```python3 -tab10 -``` - -```python3 -tab10_r -``` - -```python3 -tab20 -``` - -```python3 -tab20_r -``` - -```python3 -tab20b -``` - -```python3 -tab20b_r -``` - -```python3 -tab20c -``` - -```python3 -tab20c_r -``` - -```python3 -terrain -``` - -```python3 -terrain_r -``` - -```python3 -twilight -``` - -```python3 -twilight_r -``` - -```python3 -twilight_shifted -``` - -```python3 -twilight_shifted_r -``` - -```python3 -value -``` - -```python3 -viridis -``` - -```python3 -viridis_r -``` - -```python3 -winter -``` - -```python3 -winter_r -``` - -```python3 -wistia -``` - -```python3 -wistia_r -``` - -```python3 -ylgn -``` - -```python3 -ylgn_r -``` - -```python3 -ylgnbu -``` - -```python3 -ylgnbu_r -``` - -```python3 -ylorbr -``` - -```python3 -ylorbr_r -``` - -```python3 -ylorrd -``` - -```python3 -ylorrd_r -``` - -### DatasetParams - -```python3 -class DatasetParams( - nodata: Union[str, int, float, NoneType] = Query(None), - unscale: Union[bool, NoneType] = Query(False), - resampling_method: titiler.core.dependencies.ResamplingName = Query(ResamplingName.nearest) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.DefaultDependency - -#### Class variables - -```python3 -nodata -``` - -```python3 -resampling_method -``` - -```python3 -unscale -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### DefaultDependency - -```python3 -class DefaultDependency( - -) -``` - -#### Descendants - -* titiler.core.dependencies.BidxParams -* titiler.core.dependencies.ExpressionParams -* titiler.core.dependencies.AssetsParams -* titiler.core.dependencies.AssetsBidxExprParams -* titiler.core.dependencies.BandsParams -* titiler.core.dependencies.ImageParams -* titiler.core.dependencies.DatasetParams -* titiler.core.dependencies.ImageRenderingParams -* titiler.core.dependencies.PostProcessParams - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### ExpressionParams - -```python3 -class ExpressionParams( - expression: Union[str, NoneType] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.DefaultDependency - -#### Descendants - -* titiler.core.dependencies.BidxExprParams -* titiler.core.dependencies.BandsExprParamsOptional -* titiler.core.dependencies.BandsExprParams - -#### Class variables - -```python3 -expression -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### ImageParams - -```python3 -class ImageParams( - max_size: Union[int, NoneType] = Query(1024), - height: Union[int, NoneType] = Query(None), - width: Union[int, NoneType] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.DefaultDependency - -#### Class variables - -```python3 -height -``` - -```python3 -max_size -``` - -```python3 -width -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### ImageRenderingParams - -```python3 -class ImageRenderingParams( - add_mask: bool = Query(True) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.DefaultDependency - -#### Class variables - -```python3 -add_mask -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### PostProcessParams - -```python3 -class PostProcessParams( - in_range: Union[List[str], NoneType] = Query(None), - color_formula: Union[str, NoneType] = Query(None) -) -``` - -#### Ancestors (in MRO) - -* titiler.core.dependencies.DefaultDependency - -#### Class variables - -```python3 -color_formula -``` - -```python3 -in_range -``` - -#### Methods - - -#### keys - -```python3 -def keys( - self -) -``` - - -Return Keys. - -### ResamplingName - -```python3 -class ResamplingName( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* enum.Enum - -#### Class variables - -```python3 -average -``` - -```python3 -bilinear -``` - -```python3 -cubic -``` - -```python3 -cubic_spline -``` - -```python3 -gauss -``` - -```python3 -lanczos -``` - -```python3 -max -``` - -```python3 -med -``` - -```python3 -min -``` - -```python3 -mode -``` - -```python3 -name -``` - -```python3 -nearest -``` - -```python3 -q1 -``` - -```python3 -q3 -``` - -```python3 -rms -``` - -```python3 -sum -``` - -```python3 -value -``` - -### TileMatrixSetName - -```python3 -class TileMatrixSetName( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* enum.Enum - -#### Class variables - -```python3 -CanadianNAD83_LCC -``` - -```python3 -EuropeanETRS89_LAEAQuad -``` - -```python3 -LINZAntarticaMapTilegrid -``` - -```python3 -NZTM2000 -``` - -```python3 -NZTM2000Quad -``` - -```python3 -UPSAntarcticWGS84Quad -``` - -```python3 -UPSArcticWGS84Quad -``` - -```python3 -UTM31WGS84Quad -``` - -```python3 -WebMercatorQuad -``` - -```python3 -WorldCRS84Quad -``` - -```python3 -WorldMercatorWGS84Quad -``` - -```python3 -name -``` - -```python3 -value -``` - -### WebMercatorTileMatrixSetName - -```python3 -class WebMercatorTileMatrixSetName( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* enum.Enum - -#### Class variables - -```python3 -WebMercatorQuad -``` - -```python3 -name -``` - -```python3 -value -``` \ No newline at end of file diff --git a/docs/src/api/titiler/core/errors.md b/docs/src/api/titiler/core/errors.md index 6b897df9c..16fe3296f 100644 --- a/docs/src/api/titiler/core/errors.md +++ b/docs/src/api/titiler/core/errors.md @@ -1,162 +1,2 @@ -# Module titiler.core.errors -Titiler error classes. - -None - -## Variables - -```python3 -DEFAULT_STATUS_CODES -``` - -```python3 -logger -``` - -## Functions - - -### add_exception_handlers - -```python3 -def add_exception_handlers( - app: fastapi.applications.FastAPI, - status_codes: Dict[Type[Exception], int] -) -> None -``` - - -Add exception handlers to the FastAPI app. - - -### exception_handler_factory - -```python3 -def exception_handler_factory( - status_code: int -) -> Callable -``` - - -Create a FastAPI exception handler from a status code. - -## Classes - -### BadRequestError - -```python3 -class BadRequestError( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* titiler.core.errors.TilerError -* builtins.Exception -* builtins.BaseException - -#### Class variables - -```python3 -args -``` - -#### Methods - - -#### with_traceback - -```python3 -def with_traceback( - ... -) -``` - - -Exception.with_traceback(tb) -- - -set self.__traceback__ to tb and return self. - -### TileNotFoundError - -```python3 -class TileNotFoundError( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* titiler.core.errors.TilerError -* builtins.Exception -* builtins.BaseException - -#### Class variables - -```python3 -args -``` - -#### Methods - - -#### with_traceback - -```python3 -def with_traceback( - ... -) -``` - - -Exception.with_traceback(tb) -- - -set self.__traceback__ to tb and return self. - -### TilerError - -```python3 -class TilerError( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* builtins.Exception -* builtins.BaseException - -#### Descendants - -* titiler.core.errors.TileNotFoundError -* titiler.core.errors.BadRequestError - -#### Class variables - -```python3 -args -``` - -#### Methods - - -#### with_traceback - -```python3 -def with_traceback( - ... -) -``` - - -Exception.with_traceback(tb) -- - -set self.__traceback__ to tb and return self. \ No newline at end of file +::: titiler.core.errors diff --git a/docs/src/api/titiler/core/factory.md b/docs/src/api/titiler/core/factory.md index 5f6737af5..80ff1f59d 100644 --- a/docs/src/api/titiler/core/factory.md +++ b/docs/src/api/titiler/core/factory.md @@ -1,1000 +1,2 @@ -# Module titiler.core.factory -TiTiler Router factories. - -None - -## Variables - -```python3 -img_endpoint_params -``` - -```python3 -templates -``` - -## Classes - -### BaseTilerFactory - -```python3 -class BaseTilerFactory( - reader: Type[rio_tiler.io.base.BaseReader], - reader_options: Dict = , - router: fastapi.routing.APIRouter = , - path_dependency: Callable[..., str] = , - dataset_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - layer_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - render_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - colormap_dependency: Callable[..., Union[Dict, NoneType]] = , - process_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - tms_dependency: Callable[..., morecantile.models.TileMatrixSet] = , - additional_dependency: Callable[..., Dict] = at 0x163f42310>, - router_prefix: str = '', - gdal_config: Dict = , - optional_headers: List[titiler.core.resources.enums.OptionalHeader] = -) -``` - -#### Descendants - -* titiler.core.factory.TilerFactory - -#### Class variables - -```python3 -dataset_dependency -``` - -```python3 -layer_dependency -``` - -```python3 -process_dependency -``` - -```python3 -render_dependency -``` - -```python3 -router_prefix -``` - -#### Methods - - -#### additional_dependency - -```python3 -def additional_dependency( - -) -``` - - - - -#### colormap_dependency - -```python3 -def colormap_dependency( - colormap_name: titiler.core.dependencies.ColorMapName = Query(None), - colormap: str = Query(None) -) -> Union[Dict, Sequence, NoneType] -``` - - -Colormap Dependency. - - -#### path_dependency - -```python3 -def path_dependency( - url: str = Query(Ellipsis) -) -> str -``` - - -Create dataset path from args - - -#### register_routes - -```python3 -def register_routes( - self -) -``` - - -Register Tiler Routes. - - -#### tms_dependency - -```python3 -def tms_dependency( - TileMatrixSetId: titiler.core.dependencies.WebMercatorTileMatrixSetName = Query(WebMercatorTileMatrixSetName.WebMercatorQuad) -) -> morecantile.models.TileMatrixSet -``` - - -TileMatrixSet Dependency. - - -#### url_for - -```python3 -def url_for( - self, - request: starlette.requests.Request, - name: str, - **path_params: Any -) -> str -``` - - -Return full url (with prefix) for a specific endpoint. - -### MultiBandTilerFactory - -```python3 -class MultiBandTilerFactory( - reader: Type[rio_tiler.io.base.MultiBandReader] = , - reader_options: Dict = , - router: fastapi.routing.APIRouter = , - path_dependency: Callable[..., str] = , - dataset_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - layer_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - render_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - colormap_dependency: Callable[..., Union[Dict, NoneType]] = , - process_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - tms_dependency: Callable[..., morecantile.models.TileMatrixSet] = , - additional_dependency: Callable[..., Dict] = at 0x163f42310>, - router_prefix: str = '', - gdal_config: Dict = , - optional_headers: List[titiler.core.resources.enums.OptionalHeader] = , - img_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - add_preview: bool = True, - add_part: bool = True, - add_statistics: bool = True, - bands_dependency: Type[titiler.core.dependencies.DefaultDependency] = -) -``` - -#### Ancestors (in MRO) - -* titiler.core.factory.TilerFactory -* titiler.core.factory.BaseTilerFactory - -#### Class variables - -```python3 -add_part -``` - -```python3 -add_preview -``` - -```python3 -add_statistics -``` - -```python3 -bands_dependency -``` - -```python3 -dataset_dependency -``` - -```python3 -img_dependency -``` - -```python3 -layer_dependency -``` - -```python3 -process_dependency -``` - -```python3 -reader -``` - -```python3 -render_dependency -``` - -```python3 -router_prefix -``` - -#### Methods - - -#### additional_dependency - -```python3 -def additional_dependency( - -) -``` - - - - -#### bounds - -```python3 -def bounds( - self -) -``` - - -Register /bounds endpoint. - - -#### colormap_dependency - -```python3 -def colormap_dependency( - colormap_name: titiler.core.dependencies.ColorMapName = Query(None), - colormap: str = Query(None) -) -> Union[Dict, Sequence, NoneType] -``` - - -Colormap Dependency. - - -#### info - -```python3 -def info( - self -) -``` - - -Register /info endpoint. - - -#### part - -```python3 -def part( - self -) -``` - - -Register /crop endpoint. - - -#### path_dependency - -```python3 -def path_dependency( - url: str = Query(Ellipsis) -) -> str -``` - - -Create dataset path from args - - -#### point - -```python3 -def point( - self -) -``` - - -Register /point endpoints. - - -#### preview - -```python3 -def preview( - self -) -``` - - -Register /preview endpoint. - - -#### register_routes - -```python3 -def register_routes( - self -) -``` - - -This Method register routes to the router. - -Because we wrap the endpoints in a class we cannot define the routes as -methods (because of the self argument). The HACK is to define routes inside -the class method and register them after the class initialisation. - - -#### statistics - -```python3 -def statistics( - self -) -``` - - -add statistics endpoints. - - -#### tile - -```python3 -def tile( - self -) -``` - - -Register /tiles endpoint. - - -#### tilejson - -```python3 -def tilejson( - self -) -``` - - -Register /tilejson.json endpoint. - - -#### tms_dependency - -```python3 -def tms_dependency( - TileMatrixSetId: titiler.core.dependencies.TileMatrixSetName = Query(TileMatrixSetName.WebMercatorQuad) -) -> morecantile.models.TileMatrixSet -``` - - -TileMatrixSet Dependency. - - -#### url_for - -```python3 -def url_for( - self, - request: starlette.requests.Request, - name: str, - **path_params: Any -) -> str -``` - - -Return full url (with prefix) for a specific endpoint. - - -#### wmts - -```python3 -def wmts( - self -) -``` - - -Register /wmts endpoint. - -### MultiBaseTilerFactory - -```python3 -class MultiBaseTilerFactory( - reader: Type[rio_tiler.io.base.MultiBaseReader] = , - reader_options: Dict = , - router: fastapi.routing.APIRouter = , - path_dependency: Callable[..., str] = , - dataset_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - layer_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - render_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - colormap_dependency: Callable[..., Union[Dict, NoneType]] = , - process_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - tms_dependency: Callable[..., morecantile.models.TileMatrixSet] = , - additional_dependency: Callable[..., Dict] = at 0x163f42310>, - router_prefix: str = '', - gdal_config: Dict = , - optional_headers: List[titiler.core.resources.enums.OptionalHeader] = , - img_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - add_preview: bool = True, - add_part: bool = True, - add_statistics: bool = True, - assets_dependency: Type[titiler.core.dependencies.DefaultDependency] = -) -``` - -#### Ancestors (in MRO) - -* titiler.core.factory.TilerFactory -* titiler.core.factory.BaseTilerFactory - -#### Class variables - -```python3 -add_part -``` - -```python3 -add_preview -``` - -```python3 -add_statistics -``` - -```python3 -assets_dependency -``` - -```python3 -dataset_dependency -``` - -```python3 -img_dependency -``` - -```python3 -layer_dependency -``` - -```python3 -process_dependency -``` - -```python3 -reader -``` - -```python3 -render_dependency -``` - -```python3 -router_prefix -``` - -#### Methods - - -#### additional_dependency - -```python3 -def additional_dependency( - -) -``` - - - - -#### bounds - -```python3 -def bounds( - self -) -``` - - -Register /bounds endpoint. - - -#### colormap_dependency - -```python3 -def colormap_dependency( - colormap_name: titiler.core.dependencies.ColorMapName = Query(None), - colormap: str = Query(None) -) -> Union[Dict, Sequence, NoneType] -``` - - -Colormap Dependency. - - -#### info - -```python3 -def info( - self -) -``` - - -Register /info endpoint. - - -#### part - -```python3 -def part( - self -) -``` - - -Register /crop endpoint. - - -#### path_dependency - -```python3 -def path_dependency( - url: str = Query(Ellipsis) -) -> str -``` - - -Create dataset path from args - - -#### point - -```python3 -def point( - self -) -``` - - -Register /point endpoints. - - -#### preview - -```python3 -def preview( - self -) -``` - - -Register /preview endpoint. - - -#### register_routes - -```python3 -def register_routes( - self -) -``` - - -This Method register routes to the router. - -Because we wrap the endpoints in a class we cannot define the routes as -methods (because of the self argument). The HACK is to define routes inside -the class method and register them after the class initialisation. - - -#### statistics - -```python3 -def statistics( - self -) -``` - - -Register /statistics endpoint. - - -#### tile - -```python3 -def tile( - self -) -``` - - -Register /tiles endpoint. - - -#### tilejson - -```python3 -def tilejson( - self -) -``` - - -Register /tilejson.json endpoint. - - -#### tms_dependency - -```python3 -def tms_dependency( - TileMatrixSetId: titiler.core.dependencies.TileMatrixSetName = Query(TileMatrixSetName.WebMercatorQuad) -) -> morecantile.models.TileMatrixSet -``` - - -TileMatrixSet Dependency. - - -#### url_for - -```python3 -def url_for( - self, - request: starlette.requests.Request, - name: str, - **path_params: Any -) -> str -``` - - -Return full url (with prefix) for a specific endpoint. - - -#### wmts - -```python3 -def wmts( - self -) -``` - - -Register /wmts endpoint. - -### TMSFactory - -```python3 -class TMSFactory( - supported_tms: Type[titiler.core.dependencies.TileMatrixSetName] = , - tms_dependency: Callable[..., morecantile.models.TileMatrixSet] = , - router: fastapi.routing.APIRouter = , - router_prefix: str = '' -) -``` - -#### Class variables - -```python3 -router_prefix -``` - -```python3 -supported_tms -``` - -#### Methods - - -#### register_routes - -```python3 -def register_routes( - self -) -``` - - -Register TMS endpoint routes. - - -#### tms_dependency - -```python3 -def tms_dependency( - TileMatrixSetId: titiler.core.dependencies.TileMatrixSetName = Query(TileMatrixSetName.WebMercatorQuad) -) -> morecantile.models.TileMatrixSet -``` - - -TileMatrixSet Dependency. - - -#### url_for - -```python3 -def url_for( - self, - request: starlette.requests.Request, - name: str, - **path_params: Any -) -> str -``` - - -Return full url (with prefix) for a specific endpoint. - -### TilerFactory - -```python3 -class TilerFactory( - reader: Type[rio_tiler.io.base.BaseReader] = , - reader_options: Dict = , - router: fastapi.routing.APIRouter = , - path_dependency: Callable[..., str] = , - dataset_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - layer_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - render_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - colormap_dependency: Callable[..., Union[Dict, NoneType]] = , - process_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - tms_dependency: Callable[..., morecantile.models.TileMatrixSet] = , - additional_dependency: Callable[..., Dict] = at 0x163f42310>, - router_prefix: str = '', - gdal_config: Dict = , - optional_headers: List[titiler.core.resources.enums.OptionalHeader] = , - img_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - add_preview: bool = True, - add_part: bool = True, - add_statistics: bool = True -) -``` - -#### Ancestors (in MRO) - -* titiler.core.factory.BaseTilerFactory - -#### Descendants - -* titiler.core.factory.MultiBaseTilerFactory -* titiler.core.factory.MultiBandTilerFactory - -#### Class variables - -```python3 -add_part -``` - -```python3 -add_preview -``` - -```python3 -add_statistics -``` - -```python3 -dataset_dependency -``` - -```python3 -img_dependency -``` - -```python3 -layer_dependency -``` - -```python3 -process_dependency -``` - -```python3 -reader -``` - -```python3 -render_dependency -``` - -```python3 -router_prefix -``` - -#### Methods - - -#### additional_dependency - -```python3 -def additional_dependency( - -) -``` - - - - -#### bounds - -```python3 -def bounds( - self -) -``` - - -Register /bounds endpoint. - - -#### colormap_dependency - -```python3 -def colormap_dependency( - colormap_name: titiler.core.dependencies.ColorMapName = Query(None), - colormap: str = Query(None) -) -> Union[Dict, Sequence, NoneType] -``` - - -Colormap Dependency. - - -#### info - -```python3 -def info( - self -) -``` - - -Register /info endpoint. - - -#### part - -```python3 -def part( - self -) -``` - - -Register /crop endpoint. - - -#### path_dependency - -```python3 -def path_dependency( - url: str = Query(Ellipsis) -) -> str -``` - - -Create dataset path from args - - -#### point - -```python3 -def point( - self -) -``` - - -Register /point endpoints. - - -#### preview - -```python3 -def preview( - self -) -``` - - -Register /preview endpoint. - - -#### register_routes - -```python3 -def register_routes( - self -) -``` - - -This Method register routes to the router. - -Because we wrap the endpoints in a class we cannot define the routes as -methods (because of the self argument). The HACK is to define routes inside -the class method and register them after the class initialisation. - - -#### statistics - -```python3 -def statistics( - self -) -``` - - -add statistics endpoints. - - -#### tile - -```python3 -def tile( - self -) -``` - - -Register /tiles endpoint. - - -#### tilejson - -```python3 -def tilejson( - self -) -``` - - -Register /tilejson.json endpoint. - - -#### tms_dependency - -```python3 -def tms_dependency( - TileMatrixSetId: titiler.core.dependencies.TileMatrixSetName = Query(TileMatrixSetName.WebMercatorQuad) -) -> morecantile.models.TileMatrixSet -``` - - -TileMatrixSet Dependency. - - -#### url_for - -```python3 -def url_for( - self, - request: starlette.requests.Request, - name: str, - **path_params: Any -) -> str -``` - - -Return full url (with prefix) for a specific endpoint. - - -#### wmts - -```python3 -def wmts( - self -) -``` - - -Register /wmts endpoint. \ No newline at end of file +::: titiler.core.factory diff --git a/docs/src/api/titiler/core/middleware.md b/docs/src/api/titiler/core/middleware.md new file mode 100644 index 000000000..9c06108b0 --- /dev/null +++ b/docs/src/api/titiler/core/middleware.md @@ -0,0 +1,2 @@ + +::: titiler.core.middleware diff --git a/docs/src/api/titiler/core/models/OGC.md b/docs/src/api/titiler/core/models/OGC.md new file mode 100644 index 000000000..a0fcf3bdb --- /dev/null +++ b/docs/src/api/titiler/core/models/OGC.md @@ -0,0 +1,2 @@ + +::: titiler.core.models.OGC diff --git a/docs/src/api/titiler/core/models/mapbox.md b/docs/src/api/titiler/core/models/mapbox.md new file mode 100644 index 000000000..67b9ac6e3 --- /dev/null +++ b/docs/src/api/titiler/core/models/mapbox.md @@ -0,0 +1,2 @@ + +::: titiler.core.models.mapbox diff --git a/docs/src/api/titiler/core/models/responses.md b/docs/src/api/titiler/core/models/responses.md new file mode 100644 index 000000000..fc34ecb70 --- /dev/null +++ b/docs/src/api/titiler/core/models/responses.md @@ -0,0 +1,2 @@ + +::: titiler.core.models.responses diff --git a/docs/src/api/titiler/core/resources/enums.md b/docs/src/api/titiler/core/resources/enums.md index eb427e10f..1faab5f5d 100644 --- a/docs/src/api/titiler/core/resources/enums.md +++ b/docs/src/api/titiler/core/resources/enums.md @@ -1,239 +1,2 @@ -# Module titiler.core.resources.enums -Titiler.core Enums. - -None - -## Classes - -### ImageDriver - -```python3 -class ImageDriver( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* builtins.str -* enum.Enum - -#### Class variables - -```python3 -jp2 -``` - -```python3 -jpeg -``` - -```python3 -jpg -``` - -```python3 -name -``` - -```python3 -npy -``` - -```python3 -png -``` - -```python3 -pngraw -``` - -```python3 -tif -``` - -```python3 -value -``` - -```python3 -webp -``` - -### ImageType - -```python3 -class ImageType( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* builtins.str -* enum.Enum - -#### Class variables - -```python3 -jp2 -``` - -```python3 -jpeg -``` - -```python3 -jpg -``` - -```python3 -name -``` - -```python3 -npy -``` - -```python3 -png -``` - -```python3 -pngraw -``` - -```python3 -tif -``` - -```python3 -value -``` - -```python3 -webp -``` - -### MediaType - -```python3 -class MediaType( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* builtins.str -* enum.Enum - -#### Class variables - -```python3 -geojson -``` - -```python3 -html -``` - -```python3 -jp2 -``` - -```python3 -jpeg -``` - -```python3 -jpg -``` - -```python3 -json -``` - -```python3 -mvt -``` - -```python3 -name -``` - -```python3 -npy -``` - -```python3 -pbf -``` - -```python3 -png -``` - -```python3 -pngraw -``` - -```python3 -text -``` - -```python3 -tif -``` - -```python3 -value -``` - -```python3 -webp -``` - -```python3 -xml -``` - -### OptionalHeader - -```python3 -class OptionalHeader( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* builtins.str -* enum.Enum - -#### Class variables - -```python3 -name -``` - -```python3 -server_timing -``` - -```python3 -value -``` - -```python3 -x_assets -``` \ No newline at end of file +::: titiler.core.resources.enums diff --git a/docs/src/api/titiler/core/resources/responses.md b/docs/src/api/titiler/core/resources/responses.md new file mode 100644 index 000000000..e3ddaa410 --- /dev/null +++ b/docs/src/api/titiler/core/resources/responses.md @@ -0,0 +1,2 @@ + +::: titiler.core.resources.responses diff --git a/docs/src/api/titiler/core/routing.md b/docs/src/api/titiler/core/routing.md index 839b09283..89e89a435 100644 --- a/docs/src/api/titiler/core/routing.md +++ b/docs/src/api/titiler/core/routing.md @@ -1,26 +1,2 @@ -# Module titiler.core.routing -Custom routing classes. - -None - -## Functions - - -### apiroute_factory - -```python3 -def apiroute_factory( - env: Union[Dict, NoneType] = None -) -> Type[fastapi.routing.APIRoute] -``` - - -Create Custom API Route class with custom Env. - -Because we cannot create middleware for specific router we need to create -a custom APIRoute which add the `rasterio.Env(` block before the endpoint is -actually called. This way we set the env outside the threads and we make sure -that event multithreaded Reader will get the environment set. - -Note: This has been tested in python 3.6 and 3.7 only. \ No newline at end of file +::: titiler.core.routing diff --git a/docs/src/api/titiler/extensions/cogeo.md b/docs/src/api/titiler/extensions/cogeo.md new file mode 100644 index 000000000..7c24ed38a --- /dev/null +++ b/docs/src/api/titiler/extensions/cogeo.md @@ -0,0 +1 @@ +::: titiler.extensions.cogeo diff --git a/docs/src/api/titiler/extensions/stac.md b/docs/src/api/titiler/extensions/stac.md new file mode 100644 index 000000000..2f930fc6e --- /dev/null +++ b/docs/src/api/titiler/extensions/stac.md @@ -0,0 +1 @@ +::: titiler.extensions.stac diff --git a/docs/src/api/titiler/extensions/viewer.md b/docs/src/api/titiler/extensions/viewer.md new file mode 100644 index 000000000..e303253e3 --- /dev/null +++ b/docs/src/api/titiler/extensions/viewer.md @@ -0,0 +1 @@ +::: titiler.extensions.viewer diff --git a/docs/src/api/titiler/mosaic/errors.md b/docs/src/api/titiler/mosaic/errors.md index 2f3e10c48..e1ef9b87e 100644 --- a/docs/src/api/titiler/mosaic/errors.md +++ b/docs/src/api/titiler/mosaic/errors.md @@ -1,11 +1 @@ -# Module titiler.mosaic.errors - -Titiler mosaic errors. - -None - -## Variables - -```python3 -MOSAIC_STATUS_CODES -``` \ No newline at end of file +::: titiler.mosaic.errors diff --git a/docs/src/api/titiler/mosaic/factory.md b/docs/src/api/titiler/mosaic/factory.md index ad00ccce1..cf1ddcc8b 100644 --- a/docs/src/api/titiler/mosaic/factory.md +++ b/docs/src/api/titiler/mosaic/factory.md @@ -1,353 +1 @@ -# Module titiler.mosaic.factory - -TiTiler.mosaic Router factories. - -None - -## Variables - -```python3 -MAX_THREADS -``` - -```python3 -img_endpoint_params -``` - -```python3 -mosaic_tms -``` - -## Functions - - -### PixelSelectionParams - -```python3 -def PixelSelectionParams( - pixel_selection: titiler.mosaic.resources.enums.PixelSelectionMethod = Query(first) -) -> rio_tiler.mosaic.methods.base.MosaicMethodBase -``` - - -Returns the mosaic method used to combine datasets together. - -## Classes - -### MosaicTilerFactory - -```python3 -class MosaicTilerFactory( - reader: Type[cogeo_mosaic.backends.base.BaseBackend] = , - router: fastapi.routing.APIRouter = , - path_dependency: Callable[..., Any] = , - dataset_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - layer_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - render_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - colormap_dependency: Callable[..., Union[Dict[int, Tuple[int, int, int, int]], Sequence[Tuple[Tuple[Union[float, int], Union[float, int]], Tuple[int, int, int, int]]], NoneType]] = , - process_dependency: Callable[..., Optional[titiler.core.algorithm.base.BaseAlgorithm]] = .post_process at 0x146adc670>, - reader_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - environment_dependency: Callable[..., Dict] = at 0x146adc5e0>, - supported_tms: morecantile.defaults.TileMatrixSets = TileMatrixSets(tms={'WebMercatorQuad': }), - default_tms: str = 'WebMercatorQuad', - router_prefix: str = '', - optional_headers: List[titiler.core.resources.enums.OptionalHeader] = , - route_dependencies: List[Tuple[List[titiler.core.routing.EndpointScope], List[fastapi.params.Depends]]] = , - extensions: List[titiler.core.factory.FactoryExtension] = , - dataset_reader: Union[Type[rio_tiler.io.base.BaseReader], Type[rio_tiler.io.base.MultiBaseReader], Type[rio_tiler.io.base.MultiBandReader]] = , - backend_dependency: Type[titiler.core.dependencies.DefaultDependency] = , - pixel_selection_dependency: Callable[..., rio_tiler.mosaic.methods.base.MosaicMethodBase] = , - add_viewer: bool = True -) -``` - -#### Ancestors (in MRO) - -* titiler.core.factory.BaseTilerFactory - -#### Class variables - -```python3 -add_viewer -``` - -```python3 -backend_dependency -``` - -```python3 -dataset_dependency -``` - -```python3 -dataset_reader -``` - -```python3 -default_tms -``` - -```python3 -layer_dependency -``` - -```python3 -reader_dependency -``` - -```python3 -render_dependency -``` - -```python3 -router_prefix -``` - -```python3 -supported_tms -``` - -#### Methods - - -#### add_route_dependencies - -```python3 -def add_route_dependencies( - self, - *, - scopes: List[titiler.core.routing.EndpointScope], - dependencies=typing.List[fastapi.params.Depends] -) -``` - - -Add dependencies to routes. - -Allows a developer to add dependencies to a route after the route has been defined. - - -#### assets - -```python3 -def assets( - self -) -``` - - -Register /assets endpoint. - - -#### bounds - -```python3 -def bounds( - self -) -``` - - -Register /bounds endpoint. - - -#### colormap_dependency - -```python3 -def colormap_dependency( - colormap_name: titiler.core.dependencies.ColorMapName = Query(None), - colormap: str = Query(None) -) -> Union[Dict[int, Tuple[int, int, int, int]], Sequence[Tuple[Tuple[Union[float, int], Union[float, int]], Tuple[int, int, int, int]]], NoneType] -``` - - -Colormap Dependency. - - -#### environment_dependency - -```python3 -def environment_dependency( - -) -``` - - - - -#### info - -```python3 -def info( - self -) -``` - - -Register /info endpoint - - -#### map_viewer - -```python3 -def map_viewer( - self -) -``` - - -Register /map endpoint. - - -#### path_dependency - -```python3 -def path_dependency( - url: str = Query(Ellipsis) -) -> str -``` - - -Create dataset path from args - - -#### pixel_selection_dependency - -```python3 -def pixel_selection_dependency( - pixel_selection: titiler.mosaic.resources.enums.PixelSelectionMethod = Query(first) -) -> rio_tiler.mosaic.methods.base.MosaicMethodBase -``` - - -Returns the mosaic method used to combine datasets together. - - -#### point - -```python3 -def point( - self -) -``` - - -Register /point endpoint. - - -#### process_dependency - -```python3 -def process_dependency( - algorithm: Literal['hillshade', 'contours', 'normalizedIndex', 'terrarium', 'terrainrgb'] = Query(None), - algorithm_params: str = Query(None) -) -> Optional[titiler.core.algorithm.base.BaseAlgorithm] -``` - - -Data Post-Processing options. - - -#### read - -```python3 -def read( - self -) -``` - - -Register / (Get) Read endpoint. - - -#### reader - -```python3 -def reader( - input: str, - *args: Any, - **kwargs: Any -) -> cogeo_mosaic.backends.base.BaseBackend -``` - - -Select mosaic backend for input. - - -#### register_routes - -```python3 -def register_routes( - self -) -``` - - -This Method register routes to the router. - -Because we wrap the endpoints in a class we cannot define the routes as -methods (because of the self argument). The HACK is to define routes inside -the class method and register them after the class initialization. - - -#### tile - -```python3 -def tile( - self -) -``` - - -Register /tiles endpoints. - - -#### tilejson - -```python3 -def tilejson( - self -) -``` - - -Add tilejson endpoint. - - -#### url_for - -```python3 -def url_for( - self, - request: starlette.requests.Request, - name: str, - **path_params: Any -) -> str -``` - - -Return full url (with prefix) for a specific endpoint. - - -#### validate - -```python3 -def validate( - self -) -``` - - -Register /validate endpoint. - - -#### wmts - -```python3 -def wmts( - self -) -``` - - -Add wmts endpoint. \ No newline at end of file +::: titiler.mosaic.factory diff --git a/docs/src/api/titiler/mosaic/models/responses.md b/docs/src/api/titiler/mosaic/models/responses.md new file mode 100644 index 000000000..2b32fc0c6 --- /dev/null +++ b/docs/src/api/titiler/mosaic/models/responses.md @@ -0,0 +1 @@ +::: titiler.mosaic.models.responses diff --git a/docs/src/api/titiler/mosaic/resources/enums.md b/docs/src/api/titiler/mosaic/resources/enums.md deleted file mode 100644 index d25733aeb..000000000 --- a/docs/src/api/titiler/mosaic/resources/enums.md +++ /dev/null @@ -1,56 +0,0 @@ -# Module titiler.mosaic.resources.enums - -Titiler.mosaic Enums. - -None - -## Classes - -### PixelSelectionMethod - -```python3 -class PixelSelectionMethod( - /, - *args, - **kwargs -) -``` - -#### Ancestors (in MRO) - -* builtins.str -* enum.Enum - -#### Class variables - -```python3 -first -``` - -```python3 -highest -``` - -```python3 -lowest -``` - -```python3 -mean -``` - -```python3 -median -``` - -```python3 -name -``` - -```python3 -stdev -``` - -```python3 -value -``` \ No newline at end of file diff --git a/docs/src/api/titiler/xarray/dependencies.md b/docs/src/api/titiler/xarray/dependencies.md new file mode 100644 index 000000000..6bb4bf4a6 --- /dev/null +++ b/docs/src/api/titiler/xarray/dependencies.md @@ -0,0 +1 @@ +::: titiler.xarray.dependencies diff --git a/docs/src/api/titiler/xarray/extensions.md b/docs/src/api/titiler/xarray/extensions.md new file mode 100644 index 000000000..e6b41dc30 --- /dev/null +++ b/docs/src/api/titiler/xarray/extensions.md @@ -0,0 +1 @@ +::: titiler.xarray.extensions diff --git a/docs/src/api/titiler/xarray/factory.md b/docs/src/api/titiler/xarray/factory.md new file mode 100644 index 000000000..74f2363b4 --- /dev/null +++ b/docs/src/api/titiler/xarray/factory.md @@ -0,0 +1 @@ +::: titiler.xarray.factory diff --git a/docs/src/api/titiler/xarray/io.md b/docs/src/api/titiler/xarray/io.md new file mode 100644 index 000000000..37ccfec84 --- /dev/null +++ b/docs/src/api/titiler/xarray/io.md @@ -0,0 +1 @@ +::: titiler.xarray.io diff --git a/docs/src/benchmark.html b/docs/src/benchmark.html new file mode 100644 index 000000000..9c1d0cb5c --- /dev/null +++ b/docs/src/benchmark.html @@ -0,0 +1,292 @@ + + + + + + + Benchmarks + + + + +
+ + + + + diff --git a/docs/src/deployment/aws/ecs.md b/docs/src/deployment/aws/ecs.md deleted file mode 100644 index d89b98e1a..000000000 --- a/docs/src/deployment/aws/ecs.md +++ /dev/null @@ -1,89 +0,0 @@ -# AWS ECS (Fargate) + ALB (Application Load Balancer) - -!!! warning - When using Fargate or vanilla ECS, you should set the number of worker carefully. Setting too high a number of workers could lead to extra charges due to a bug in fastapi (https://github.com/developmentseed/titiler/issues/119, https://github.com/tiangolo/fastapi/issues/253). - - -## Deploy - -The example handles tasks such as generating a docker image and setting up an application load balancer (ALB) and ECS services. - - -1. Install CDK and connect to your AWS account. This step is only necessary once per AWS account. - - ```bash - # Download titiler repo - $ git clone https://github.com/developmentseed/titiler.git - - # Create a virtual environment - python -m pip install --upgrade virtualenv - virtualenv .venv - source .venv/bin/activate - - # Install CDK dependencies - python -m pip install -r requirements-cdk.txt - - # Install NodeJS dependencies - npm install - - $ npm run cdk -- bootstrap # Deploys the CDK toolkit stack into an AWS environment - - # or in specific region - $ npm run cdk -- bootstrap aws://${AWS_ACCOUNT_ID}/eu-central-1 - ``` - -2. Generate CloudFormation template - - ```bash - $ npm run cdk -- synth # Synthesizes and prints the CloudFormation template for this stack - ``` - -3. Update settings (see [intro.md](intro.md)) - - ```bash - export TITILER_STACK_NAME="mytiler" - export TITILER_STACK_STAGE="dev" - export TITILER_STACK_MIN_ECS_INSTANCES=10 - ``` - - Available settings for ECS: - - ```python - min_ecs_instances: int = 5 - max_ecs_instances: int = 50 - - # CPU value | Memory value - # 256 (.25 vCPU) | 0.5 GB, 1 GB, 2 GB - # 512 (.5 vCPU) | 1 GB, 2 GB, 3 GB, 4 GB - # 1024 (1 vCPU) | 2 GB, 3 GB, 4 GB, 5 GB, 6 GB, 7 GB, 8 GB - # 2048 (2 vCPU) | Between 4 GB and 16 GB in 1-GB increments - # 4096 (4 vCPU) | Between 8 GB and 30 GB in 1-GB increments - task_cpu: int = 256 - task_memory: int = 512 - - # GUNICORN configuration - # Ref: https://github.com/developmentseed/titiler/issues/119 - - # WORKERS_PER_CORE - # This image will check how many CPU cores are available in the current server running your container. - # It will set the number of workers to the number of CPU cores multiplied by this value. - workers_per_core: int = 1 - - # MAX_WORKERS - # You can use it to let the image compute the number of workers automatically but making sure it's limited to a maximum. - # should depends on `task_cpu` - max_workers: int = 1 - - # WEB_CONCURRENCY - # Override the automatic definition of number of workers. - # Set to the number of CPU cores in the current server multiplied by the environment variable WORKERS_PER_CORE. - # So, in a server with 2 cores, by default it will be set to 2. - web_concurrency: Optional[int] - ``` - -4. Deploy - - ```bash - # Deploys the stack(s) mytiler-ecs-dev in cdk/app.py - $ npm run cdk -- deploy mytiler-ecs-dev - ``` diff --git a/docs/src/deployment/aws/intro.md b/docs/src/deployment/aws/intro.md index 5130c9fd0..63a52c11d 100644 --- a/docs/src/deployment/aws/intro.md +++ b/docs/src/deployment/aws/intro.md @@ -49,42 +49,6 @@ env: Dict = { # add S3 bucket where TiTiler could do HEAD and GET Requests buckets: List = [] -########################################################################### -# AWS ECS -# The following settings only apply to AWS ECS deployment -min_ecs_instances: int = 5 -max_ecs_instances: int = 50 - -# CPU value | Memory value -# 256 (.25 vCPU) | 0.5 GB, 1 GB, 2 GB -# 512 (.5 vCPU) | 1 GB, 2 GB, 3 GB, 4 GB -# 1024 (1 vCPU) | 2 GB, 3 GB, 4 GB, 5 GB, 6 GB, 7 GB, 8 GB -# 2048 (2 vCPU) | Between 4 GB and 16 GB in 1-GB increments -# 4096 (4 vCPU) | Between 8 GB and 30 GB in 1-GB increments -task_cpu: int = 256 -task_memory: int = 512 - -# GUNICORN configuration -# Ref: https://github.com/developmentseed/titiler/issues/119 - -# WORKERS_PER_CORE -# This image will check how many CPU cores are available in the current server running your container. -# It will set the number of workers to the number of CPU cores multiplied by this value. -workers_per_core: int = 1 - -# MAX_WORKERS -# You can use it to let the image compute the number of workers automatically but making sure it's limited to a maximum. -# should depends on `task_cpu` -max_workers: int = 1 - -# WEB_CONCURRENCY -# Override the automatic definition of number of workers. -# Set to the number of CPU cores in the current server multiplied by the environment variable WORKERS_PER_CORE. -# So, in a server with 2 cores, by default it will be set to 2. -web_concurrency: Optional[int] - -image_version: str = "latest" - ########################################################################### # AWS LAMBDA # The following settings only apply to AWS Lambda deployment diff --git a/docs/src/endpoints/algorithms.md b/docs/src/endpoints/algorithms.md new file mode 100644 index 000000000..58e375eee --- /dev/null +++ b/docs/src/endpoints/algorithms.md @@ -0,0 +1,116 @@ +In addition to the `/cog`, `/stac` and `/mosaicjson` endpoints, the `titiler.application` package FastAPI application commes with additional metadata endpoints. + +# Algorithms + +## API + +| Method | URL | Output | Description +| ------ | ---------------------------- |---------------- |-------------- +| `GET` | `/algorithms` | JSON | retrieve the list of available Algorithms +| `GET` | `/algorithms/{algorithmId}` | JSON | retrieve the metadata of the specified algorithm. + +## Description + + +### List Algorithm + +`:endpoint:/algorithm` - Get the list of supported TileMatrixSet + +```bash +$ curl https://myendpoint/algorithms | jq + +{ + "hillshade": { + "title": "Hillshade", + "description": "Create hillshade from DEM dataset.", + "inputs": { + "nbands": 1 + }, + "outputs": { + "nbands": 1, + "dtype": "uint8", + "min": null, + "max": null + }, + "parameters": { + "azimuth": { + "default": 90, + "maximum": 360, + "minimum": 0, + "title": "Azimuth", + "type": "integer" + }, + "angle_altitude": { + "default": 90.0, + "maximum": 90.0, + "minimum": -90.0, + "title": "Angle Altitude", + "type": "number" + }, + "buffer": { + "default": 3, + "maximum": 99, + "minimum": 0, + "title": "Buffer", + "type": "integer" + } + } + }, + ... +} +``` + +### Get Algorithm info + +`:endpoint:/algorithms/{algorithmId}` - Get the algorithm metadata + +- PathParams: + - **algorithmId**: algorithm name + +```bash +$ curl http://127.0.0.1:8000/algorithms/contours | jq + +{ + "title": "Contours", + "description": "Create contours from DEM dataset.", + "inputs": { + "nbands": 1 + }, + "outputs": { + "nbands": 3, + "dtype": "uint8", + "min": null, + "max": null + }, + "parameters": { + "increment": { + "default": 35, + "maximum": 999, + "minimum": 0, + "title": "Increment", + "type": "integer" + }, + "thickness": { + "default": 1, + "maximum": 10, + "minimum": 0, + "title": "Thickness", + "type": "integer" + }, + "minz": { + "default": -12000, + "maximum": 99999, + "minimum": -99999, + "title": "Minz", + "type": "integer" + }, + "maxz": { + "default": 8000, + "maximum": 99999, + "minimum": -99999, + "title": "Maxz", + "type": "integer" + } + } +} +``` diff --git a/docs/src/endpoints/cog.md b/docs/src/endpoints/cog.md index a1add20b0..7c740fe80 100644 --- a/docs/src/endpoints/cog.md +++ b/docs/src/endpoints/cog.md @@ -14,14 +14,16 @@ The `/cog` routes are based on `titiler.core.factory.TilerFactory` but with `cog | `GET` | `/cog/info.geojson` | GeoJSON | return dataset's basic info as a GeoJSON feature | `GET` | `/cog/statistics` | JSON | return dataset's statistics | `POST` | `/cog/statistics` | GeoJSON | return dataset's statistics for a GeoJSON -| `GET` | `/cog/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset -| `GET` | `/cog[/{TileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document -| `GET` | `/cog[/{TileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities +| `GET` | `/cog/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/cog/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/cog/tiles/{tileMatrixSetId}/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from a dataset +| `GET` | `/cog/{tileMatrixSetId}/map.html` | HTML | simple map viewer +| `GET` | `/cog/{tileMatrixSetId}/tilejson.json` | JSON | return a Mapbox TileJSON document +| `GET` | `/cog/{tileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities | `GET` | `/cog/point/{lon},{lat}` | JSON | return pixel values from a dataset -| `GET` | `/cog/preview[.{format}]` | image/bin | create a preview image from a dataset -| `GET` | `/cog/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset -| `POST` | `/cog/crop[/{width}x{height}][].{format}]` | image/bin | create an image from a GeoJSON feature -| `GET` | `/cog[/{TileMatrixSetId}]/map` | HTML | simple map viewer +| `GET` | `/cog/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of a dataset +| `POST` | `/cog/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a GeoJSON feature +| `GET` | `/cog/preview[/{width}x{height}][.{format}]` | image/bin | create a preview image from a dataset | `GET` | `/cog/validate` | JSON | validate a COG and return dataset info (from `titiler.extensions.cogValidateExtension`) | `GET` | `/cog/viewer` | HTML | demo webpage (from `titiler.extensions.cogViewerExtension`) | `GET` | `/cog/stac` | GeoJSON | create STAC Items from a dataset (from `titiler.extensions.stacExtension`) @@ -30,56 +32,64 @@ The `/cog` routes are based on `titiler.core.factory.TilerFactory` but with `cog ### Tiles -`:endpoint:/cog/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` +`:endpoint:/cog/tiles/{tileMatrixSetId}/{z}/{x}/{y}[@{scale}x][.{format}]` - PathParams: - - **TileMatrixSetId** (str): TileMatrixSet name, default is `WebMercatorQuad`. **Optional** + - **tileMatrixSetId** (str): TileMatrixSet name (e.g `WebMercatorQuad`) - **z** (int): TMS tile's zoom level. - **x** (int): TMS tile's column. - **y** (int): TMS tile's row. - **scale** (int): Tile size scale, default is set to 1 (256x256). **Optional** - - **format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. **Optional** + - **format** (str): Output [image format](../user_guide/output_format.md), default is set to None and will be either JPEG or PNG depending on masked value. **Optional** - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. - **colormap_name** (str): rio-tiler color map name. - **return_mask** (bool): Add mask to the output data. Default is True. - - **buffer** (float): Add buffer on each side of the tile (e.g 0.5 = 257x257, 1.0 = 258x258). + - **buffer** (float): Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258). + - **padding** (int): Padding to apply to each tile edge. Helps reduce resampling artefacts along edges. Defaults to `0`. - **algorithm** (str): Custom algorithm name (e.g `hillshade`). - **algorithm_params** (str): JSON encoded algorithm parameters. Example: -- `https://myendpoint/cog/tiles/1/2/3?url=https://somewhere.com/mycog.tif` -- `https://myendpoint/cog/tiles/1/2/3.jpg?url=https://somewhere.com/mycog.tif&bidx=3&bidx=1&bidx2` +- `https://myendpoint/cog/tiles/WebMercatorQuad/1/2/3?url=https://somewhere.com/mycog.tif` +- `https://myendpoint/cog/tiles/WebMercatorQuad/1/2/3.jpg?url=https://somewhere.com/mycog.tif&bidx=3&bidx=1&bidx2` - `https://myendpoint/cog/tiles/WorldCRS84Quad/1/2/3@2x.png?url=https://somewhere.com/mycog.tif` - `https://myendpoint/cog/tiles/WorldCRS84Quad/1/2/3?url=https://somewhere.com/mycog.tif&bidx=1&rescale=0,1000&colormap_name=cfastie` ### Preview -`:endpoint:/cog/preview[.{format}]` +`:endpoint:/cog/preview` + +`:endpoint:/cog/preview.{format}` + +`:endpoint:/cog/preview/{width}x{height}.{format}` - PathParams: - - **format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. **Optional** + - **format** (str, optional): Output [image format](../user_guide/output_format.md), default is set to None and will be either JPEG or PNG depending on masked value. **Also a QueryParam** + - **height** (int, optional): Force output image height. **Also a QueryParam** + - **width** (int, optional): Force output image width. **Also a QueryParam** - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **max_size** (int): Max image size, default is 1024. - - **height** (int): Force output image height. - - **width** (int): Force output image width. + - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. @@ -89,35 +99,38 @@ Example: - **algorithm_params** (str): JSON encoded algorithm parameters. !!! important - if **height** and **width** are provided **max_size** will be ignored. + if **height** or **width** is provided **max_size** will be ignored. Example: - `https://myendpoint/cog/preview?url=https://somewhere.com/mycog.tif` - `https://myendpoint/cog/preview.jpg?url=https://somewhere.com/mycog.tif&bidx=3&bidx=1&bidx2` +- `https://myendpoint/cog/preview/100x100.jpg?url=https://somewhere.com/mycog.tif&bidx=3&bidx=1&bidx2` - `https://myendpoint/cog/preview?url=https://somewhere.com/mycog.tif&bidx=1&rescale=0,1000&colormap_name=cfastie` -### Crop / Part +### Bbox -`:endpoint:/cog/crop/{minx},{miny},{maxx},{maxy}.{format}` +`:endpoint:/cog/bbox/{minx},{miny},{maxx},{maxy}.{format}` -`:endpoint:/cog/crop/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}` +`:endpoint:/cog/bbox/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}` - PathParams: - **minx,miny,maxx,maxy** (str): Comma (',') delimited bounding box in WGS84. - - **format** (str): Output image format. - - **height** (int): Force output image height. - - **width** (int): Force output image width. + - **format** (str): Output [image format](../user_guide/output_format.md) + - **height** (int, optional): Force output image height. **Also a QueryParam** + - **width** (int, optional): Force output image width. **Also a QueryParam** - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). - - **coord-crs** (str): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. - - **max_size** (int): Max image size, default is 1024. + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). + - **coord_crs** (str): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. + - **dst_crs** (str): Output Coordinate Reference System. Default to `coord_crs`. + - **max_size** (int): Max image size. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. @@ -127,33 +140,40 @@ Example: - **algorithm_params** (str): JSON encoded algorithm parameters. !!! important - if **height** and **width** are provided **max_size** will be ignored. + if **height** or **width** is provided **max_size** will be ignored. Example: -- `https://myendpoint/cog/crop/0,0,10,10.png?url=https://somewhere.com/mycog.tif` -- `https://myendpoint/cog/crop/0,0,10,10.png?url=https://somewhere.com/mycog.tif&bidx=1&rescale=0,1000&colormap_name=cfastie` +- `https://myendpoint/cog/bbox/0,0,10,10.png?url=https://somewhere.com/mycog.tif&bidx=1&rescale=0,1000&colormap_name=cfastie` +- `https://myendpoint/cog/bbox/0,0,10,10/100x100.png?url=https://somewhere.com/mycog.tif` + +### Feature + +`:endpoint:/cog/feature - [POST]` +`:endpoint:/cog/feature.{format} - [POST]` -`:endpoint:/cog/crop[/{width}x{height}][].{format}] - [POST]` +`:endpoint:/cog/feature/{width}x{height}.{format} - [POST]` - Body: - **feature** (JSON): A valid GeoJSON feature (Polygon or MultiPolygon) - PathParams: - - **height** (int): Force output image height. **Optional** - - **width** (int): Force output image width. **Optional** - - **format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. **Optional** + - **height** (int, optional): Force output image height. **Also a QueryParam** + - **width** (int, optional): Force output image width. **Also a QueryParam** + - **format** (str, optional): Output [image format](../user_guide/output_format.md), default is set to None and will be either JPEG or PNG depending on masked value. **Also a QueryParam** - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). - - **coord-crs** (str): Coordinate Reference System of the input geometry coordinates. Default to `epsg:4326`. - - **max_size** (int): Max image size, default is 1024. + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). + - **coord_crs** (str): Coordinate Reference System of the input geometry coordinates. Default to `epsg:4326`. + - **dst_crs** (str): Output Coordinate Reference System. Default to `coord_crs`. + - **max_size** (int): Max image size. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. @@ -163,15 +183,13 @@ Example: - **algorithm_params** (str): JSON encoded algorithm parameters. !!! important - if **height** and **width** are provided **max_size** will be ignored. + if **height** or **width** is provided **max_size** will be ignored. Example: -- `https://myendpoint/cog/crop?url=https://somewhere.com/mycog.tif` -- `https://myendpoint/cog/crop.png?url=https://somewhere.com/mycog.tif` -- `https://myendpoint/cog/crop/100x100.png?url=https://somewhere.com/mycog.tif&bidx=1&rescale=0,1000&colormap_name=cfastie` - -Note: if `height` and `width` are provided `max_size` will be ignored. +- `https://myendpoint/cog/feature?url=https://somewhere.com/mycog.tif` +- `https://myendpoint/cog/feature.png?url=https://somewhere.com/mycog.tif` +- `https://myendpoint/cog/feature/100x100.png?url=https://somewhere.com/mycog.tif&bidx=1&rescale=0,1000&colormap_name=cfastie` ### Point @@ -183,11 +201,12 @@ Note: if `height` and `width` are provided `max_size` will be ignored. - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). - - **coord-crs** (str): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). + - **coord_crs** (str): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. Example: @@ -196,70 +215,74 @@ Example: ### TilesJSON -`:endpoint:/cog[/{TileMatrixSetId}]/tilejson.json` tileJSON document +`:endpoint:/cog/{tileMatrixSetId}/tilejson.json` tileJSON document - PathParams: - - **TileMatrixSetId**: TileMatrixSet name, default is `WebMercatorQuad`. **Optional** + - **tileMatrixSetId** (str): TileMatrixSet name (e.g `WebMercatorQuad`) - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - - **tile_format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. + - **tile_format** (str): Output [image format](../user_guide/output_format.md), default is set to None and will be either JPEG or PNG depending on masked value. - **tile_scale** (int): Tile size scale, default is set to 1 (256x256). - **minzoom** (int): Overwrite default minzoom. - **maxzoom** (int): Overwrite default maxzoom. - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. - **colormap_name** (str): rio-tiler color map name. - **return_mask** (bool): Add mask to the output data. Default is True. - - **buffer** (float): Add buffer on each side of the tile (e.g 0.5 = 257x257, 1.0 = 258x258). + - **buffer** (float): Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258). + - **padding** (int): Padding to apply to each tile edge. Helps reduce resampling artefacts along edges. Defaults to `0`. - **algorithm** (str): Custom algorithm name (e.g `hillshade`). - **algorithm_params** (str): JSON encoded algorithm parameters. Example: -- `https://myendpoint/cog/tilejson.json?url=https://somewhere.com/mycog.tif` -- `https://myendpoint/cog/tilejson.json?url=https://somewhere.com/mycog.tif&tile_format=png` +- `https://myendpoint/cog/WebMercatorQuad/tilejson.json?url=https://somewhere.com/mycog.tif` +- `https://myendpoint/cog/WebMercatorQuad/tilejson.json?url=https://somewhere.com/mycog.tif&tile_format=png` - `https://myendpoint/cog/WorldCRS84Quad/tilejson.json?url=https://somewhere.com/mycog.tif&tile_scale=2&bidx=1,2,3` ### Map -`:endpoint:/cog[/{TileMatrixSetId}]/map` Simple viewer +`:endpoint:/cog/{tileMatrixSetId}/map.html` Simple viewer - PathParams: - - **TileMatrixSetId**: TileMatrixSet name, default is `WebMercatorQuad`. **Optional** + - **tileMatrixSetId** (str): TileMatrixSet name (e.g `WebMercatorQuad`) - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - - **tile_format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. + - **tile_format** (str): Output [image format](../user_guide/output_format.md), default is set to None and will be either JPEG or PNG depending on masked value. - **tile_scale** (int): Tile size scale, default is set to 1 (256x256). - **minzoom** (int): Overwrite default minzoom. - **maxzoom** (int): Overwrite default maxzoom. - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. - **colormap_name** (str): rio-tiler color map name. - **return_mask** (bool): Add mask to the output data. Default is True. - - **buffer** (float): Add buffer on each side of the tile (e.g 0.5 = 257x257, 1.0 = 258x258). + - **buffer** (float): Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258). + - **padding** (int): Padding to apply to each tile edge. Helps reduce resampling artefacts along edges. Defaults to `0`. - **algorithm** (str): Custom algorithm name (e.g `hillshade`). - **algorithm_params** (str): JSON encoded algorithm parameters. Example: -- `https://myendpoint/cog/map?url=https://somewhere.com/mycog.tif` -- `https://myendpoint/cog/map?url=https://somewhere.com/mycog.tif&tile_format=png` -- `https://myendpoint/cog/WebMercatorQuad/map?url=https://somewhere.com/mycog.tif&tile_scale=2&bidx=1,2,3` +- `https://myendpoint/cog/WebMercatorQuad/map.html?url=https://somewhere.com/mycog.tif` +- `https://myendpoint/cog/WebMercatorQuad/map.html?url=https://somewhere.com/mycog.tif&tile_format=png` +- `https://myendpoint/cog/WorldCRS84Quad/map.html?url=https://somewhere.com/mycog.tif&tile_scale=2&bidx=1,2,3` ### Bounds @@ -268,6 +291,7 @@ Example: - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** + - **crs** (str): Geographic Coordinate Reference System. Default to `epsg:4326`. Example: @@ -277,17 +301,23 @@ Example: `:endpoint:/cog/info` general raster info +- QueryParams: + - **url** (str): Cloud Optimized GeoTIFF URL. **Required** + +Example: + +- `https://myendpoint/cog/info?url=https://somewhere.com/mycog.tif` + `:endpoint:/cog/info.geojson` general raster info as a GeoJSON feature - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** + - **crs** (str): Geographic Coordinate Reference System. Default to `epsg:4326`. Example: -- `https://myendpoint/cog/info?url=https://somewhere.com/mycog.tif` - `https://myendpoint/cog/info.geojson?url=https://somewhere.com/mycog.tif` - ### Statistics Advanced raster statistics @@ -297,13 +327,15 @@ Advanced raster statistics - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). - **max_size** (int): Max image size from which to calculate statistics, default is 1024. - **height** (int): Force image height from which to calculate statistics. - **width** (int): Force image width from which to calculate statistics. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **algorithm** (str): Custom algorithm name (e.g `hillshade`). + - **algorithm_params** (str): JSON encoded algorithm parameters. - **categorical** (bool): Return statistics for categorical dataset, default is false. - **c** (array[float]): Pixels values for categories. - **p** (array[int]): Percentile values. @@ -322,14 +354,18 @@ Example: - QueryParams: - **url** (str): Cloud Optimized GeoTIFF URL. **Required** - **bidx** (array[int]): Dataset band indexes (e.g `bidx=1`, `bidx=1&bidx=2&bidx=3`). - - **expression** (str): rio-tiler's band math expression (e.g B1/B2). - - **coord-crs** (str): Coordinate Reference System of the input geometry coordinates. Default to `epsg:4326`. - - **max_size** (int): Max image size from which to calculate statistics, default is 1024. + - **expression** (str): rio-tiler's band math expression (e.g `expression=b1/b2`). + - **coord_crs** (str): Coordinate Reference System of the input geometry coordinates. Default to `epsg:4326`. + - **dst_crs** (str): Output Coordinate Reference System. Default to `coord_crs`. + - **max_size** (int): Max image size from which to calculate statistics. - **height** (int): Force image height from which to calculate statistics. - **width** (int): Force image width from which to calculate statistics. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. + - **algorithm** (str): Custom algorithm name (e.g `hillshade`). + - **algorithm_params** (str): JSON encoded algorithm parameters. - **categorical** (bool): Return statistics for categorical dataset, default is false. - **c** (array[float]): Pixels values for categories. - **p** (array[int]): Percentile values. diff --git a/docs/src/endpoints/colormaps.md b/docs/src/endpoints/colormaps.md new file mode 100644 index 000000000..8ad4f10db --- /dev/null +++ b/docs/src/endpoints/colormaps.md @@ -0,0 +1,96 @@ +In addition to the `/cog`, `/stac` and `/mosaicjson` endpoints, the `titiler.application` package FastAPI application commes with additional metadata endpoints. + +# Algorithms + +## API + +| Method | URL | Output | Description +| ------ | ---------------------------- |--------|-------------- +| `GET` | `/colorMaps` | JSON | retrieve the list of available colorMaps +| `GET` | `/colorMaps/{colorMapId}` | JSON | retrieve the metadata or image of the specified colorMap. + +## Description + + +### List colormaps + +`:endpoint:/colorMaps` - Get the list of supported ColorMaps + +```bash +$ curl https://myendpoint/colorMaps | jq + +{ + "colorMaps": [ + "dense_r", + "delta", + ... + ], + "links": [ + { + "href": "http://myendpoint/colorMaps", + "rel": "self", + "type": "application/json", + "title": "List of available colormaps" + }, + { + "href": "http://myendpoint/colorMaps/{colorMapId}", + "rel": "data", + "type": "application/json", + "templated": true, + "title": "Retrieve colormap metadata" + }, + { + "href": "http://myendpoint/colorMaps/{colorMapId}?format=png", + "rel": "data", + "type": "image/png", + "templated": true, + "title": "Retrieve colormap as image" + } + ] +} +``` + +### Get ColorMap metadata or as image + +`:endpoint:/colorMaps/{colorMapId}` - Get the ColorMap metadata or image + +- PathParams: + - **colorMapId**: colormap name + +- QueryParams: + - **format** (str): output image format (PNG/JPEG...). Defaults to JSON output. + - **orientation** (["vertical", "horizontal"]): image orientation. Defaults to `horizontal`. + - **height** (int): output image height. Default to 20px for horizontal or 256px for vertical. + - **width** (int): output image width. Defaults to 256px for horizontal or 20px for vertical. + +```bash +$ curl http://myendpoint/colorMaps/viridis | jq + +{ + "0": [ + 68, + 1, + 84, + 255 + ], + ... + "255": [ + 253, + 231, + 36, + 255 + ] +} +``` + +``` +curl http://myendpoint/colorMaps/viridis?format=png +``` + +``` +curl http://myendpoint/colorMaps/viridis?format=png&orientation=vertical +``` + +``` +curl http://myendpoint/colorMaps/viridis?format=png&orientation=vertical&width=100&height=1000 +``` diff --git a/docs/src/endpoints/mosaic.md b/docs/src/endpoints/mosaic.md index 47537129c..60e3f071b 100644 --- a/docs/src/endpoints/mosaic.md +++ b/docs/src/endpoints/mosaic.md @@ -13,14 +13,16 @@ Read Mosaic Info/Metadata and create Web map Tiles from a multiple COG. The `mos | `GET` | `/mosaicjson/bounds` | JSON | return mosaic's bounds | `GET` | `/mosaicjson/info` | JSON | return mosaic's basic info | `GET` | `/mosaicjson/info.geojson` | GeoJSON | return mosaic's basic info as a GeoJSON feature -| `GET` | `/mosaicjson/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from mosaic assets -| `GET` | `/mosaicjson[/{TileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document -| `GET` | `/mosaicjson[/{TileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities +| `GET` | `/mosaicjson/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/mosaicjson/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/mosaicjson/tiles/{tileMatrixSetId}/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from mosaic assets +| `GET` | `/mosaicjson/{tileMatrixSetId}/map.html` | HTML | simple map viewer +| `GET` | `/mosaicjson/{tileMatrixSetId}/tilejson.json` | JSON | return a Mapbox TileJSON document +| `GET` | `/mosaicjson/{tileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities | `GET` | `/mosaicjson/point/{lon},{lat}` | JSON | return pixel value from a mosaic assets -| `GET` | `/mosaicjson/{z}/{x}/{y}/assets` | JSON | return list of assets intersecting a XYZ tile -| `GET` | `/mosaicjson/{lon},{lat}/assets` | JSON | return list of assets intersecting a point -| `GET` | `/mosaicjson/{minx},{miny},{maxx},{maxy}/assets` | JSON | return list of assets intersecting a bounding box -| `GET` | `/mosaicjson[/{TileMatrixSetId}]/map` | HTML | simple map viewer +| `GET` | `/mosaicjson/tiles/{tileMatrixSetId}/{z}/{x}/{y}/assets` | JSON | return list of assets intersecting a XYZ tile +| `GET` | `/mosaicjson/point/{lon},{lat}/assets` | JSON | return list of assets intersecting a point +| `GET` | `/mosaicjson/bbox/{minx},{miny},{maxx},{maxy}/assets` | JSON | return list of assets intersecting a bounding box ## Description diff --git a/docs/src/endpoints/stac.md b/docs/src/endpoints/stac.md index b70a9700d..e7a39c0fd 100644 --- a/docs/src/endpoints/stac.md +++ b/docs/src/endpoints/stac.md @@ -16,29 +16,31 @@ The `/stac` routes are based on `titiler.core.factory.MultiBaseTilerFactory` but | `GET` | `/stac/asset_statistics` | JSON | return per asset statistics | `GET` | `/stac/statistics` | JSON | return asset's statistics | `POST` | `/stac/statistics` | GeoJSON | return asset's statistics for a GeoJSON -| `GET` | `/stac/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from assets -| `GET` | `/stac[/{TileMatrixSetId}]/tilejson.json` | JSON | return a Mapbox TileJSON document -| `GET` | `/stac[/{TileMatrixSetId}]/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities +| `GET` | `/stac/tiles` | JSON | List of OGC Tilesets available +| `GET` | `/stac/tiles/{tileMatrixSetId}` | JSON | OGC Tileset metadata +| `GET` | `/stac/tiles/{tileMatrixSetId}/{z}/{x}/{y}[@{scale}x][.{format}]` | image/bin | create a web map tile image from assets +| `GET` | `/stac/{tileMatrixSetId}/map.html` | HTML | simple map viewer +| `GET` | `/stac/{tileMatrixSetId}/tilejson.json` | JSON | return a Mapbox TileJSON document +| `GET` | `/stac/{tileMatrixSetId}/WMTSCapabilities.xml` | XML | return OGC WMTS Get Capabilities | `GET` | `/stac/point/{lon},{lat}` | JSON | return pixel value from assets -| `GET` | `/stac/preview[.{format}]` | image/bin | create a preview image from assets -| `GET` | `/stac/crop/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of assets -| `POST` | `/stac/crop[/{width}x{height}][].{format}]` | image/bin | create an image from a geojson covering the assets -| `GET` | `/stac[/{TileMatrixSetId}]/map` | HTML | simple map viewer +| `GET` | `/stac/bbox/{minx},{miny},{maxx},{maxy}[/{width}x{height}].{format}` | image/bin | create an image from part of assets +| `POST` | `/stac/feature[/{width}x{height}][.{format}]` | image/bin | create an image from a geojson covering the assets +| `GET` | `/stac/preview[/{width}x{height}][.{format}]` | image/bin | create a preview image from assets | `GET` | `/stac/viewer` | HTML | demo webpage (from `titiler.extensions.stacViewerExtension`) ## Description ### Tiles -`:endpoint:/stac/tiles[/{TileMatrixSetId}]/{z}/{x}/{y}[@{scale}x][.{format}]` +`:endpoint:/stac/tiles/{tileMatrixSetId}/{z}/{x}/{y}[@{scale}x][.{format}]` - PathParams: - - **TileMatrixSetId** (str): TileMatrixSet name, default is `WebMercatorQuad`. **Optional** + - **tileMatrixSetId** (str): TileMatrixSet name (e.g `WebMercatorQuad`) - **z** (int): TMS tile's zoom level. - **x** (int): TMS tile's column. - **y** (int): TMS tile's row. - **scale** (int): Tile size scale, default is set to 1 (256x256). **Optional** - - **format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. **Optional** + - **format** (str): Output [image format](../user_guide/output_format.md), default is set to None and will be either JPEG or PNG depending on masked value. **Optional** - QueryParams: - **url** (str): STAC Item URL. **Required** @@ -48,13 +50,15 @@ The `/stac` routes are based on `titiler.core.factory.MultiBaseTilerFactory` but - **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`). - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. - **colormap_name** (str): rio-tiler color map name. - **return_mask** (bool): Add mask to the output data. Default is True. - - **buffer** (float): Add buffer on each side of the tile (e.g 0.5 = 257x257, 1.0 = 258x258). + - **buffer** (float): Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258). + - **padding** (int): Padding to apply to each tile edge. Helps reduce resampling artefacts along edges. Defaults to `0`. - **algorithm** (str): Custom algorithm name (e.g `hillshade`). - **algorithm_params** (str): JSON encoded algorithm parameters. @@ -63,17 +67,23 @@ The `/stac` routes are based on `titiler.core.factory.MultiBaseTilerFactory` but Example: -- `https://myendpoint/stac/tiles/1/2/3?url=https://somewhere.com/item.json&assets=B01&assets=B00` -- `https://myendpoint/stac/tiles/1/2/3.jpg?url=https://somewhere.com/item.json&assets=B01` +- `https://myendpoint/stac/tiles/WebMercatorQuad/1/2/3?url=https://somewhere.com/item.json&assets=B01&assets=B00` +- `https://myendpoint/stac/tiles/WebMercatorQuad/1/2/3.jpg?url=https://somewhere.com/item.json&assets=B01` - `https://myendpoint/stac/tiles/WorldCRS84Quad/1/2/3@2x.png?url=https://somewhere.com/item.json&assets=B01` - `https://myendpoint/stac/tiles/WorldCRS84Quad/1/2/3?url=https://somewhere.com/item.json&expression=B01/B02&rescale=0,1000&colormap_name=cfastie` ### Preview -`:endpoint:/stac/preview[.{format}]` +`:endpoint:/stac/preview` + +`:endpoint:/stac/preview/.{format}` + +`:endpoint:/stac/preview/{width}x{height}.{format}` - PathParams: - - **format**: Output image format, default is set to None and will be either JPEG or PNG depending on masked value. **Optional** + - **format** (str, optional): Output [image format](../user_guide/output_format.md), default is set to None and will be either JPEG or PNG depending on masked value. **Also a QueryParam** + - **height** (int, optional): Force output image height. **Also a QueryParam** + - **width** (int, optional): Force output image width. **Also a QueryParam** - QueryParams: - **url** (str): STAC Item URL. **Required** @@ -82,11 +92,11 @@ Example: - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset, so expression `Asset1/Asset2` can be passed. - **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`). - **max_size** (int): Max image size, default is 1024. - - **height** (int): Force output image height. - - **width** (int): Force output image width. + - **dst_crs** (str): Output Coordinate Reference System. Default to dataset's CRS. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. @@ -98,25 +108,27 @@ Example: !!! important - **assets** OR **expression** is required - - if **height** and **width** are provided **max_size** will be ignored. + - if **height** or **width** is provided **max_size** will be ignored. Example: - `https://myendpoint/stac/preview?url=https://somewhere.com/item.json&assets=B01` - `https://myendpoint/stac/preview.jpg?url=https://somewhere.com/item.json&assets=B01` +- `https://myendpoint/stac/preview/100x100.jpg?url=https://somewhere.com/item.json&assets=B01` - `https://myendpoint/stac/preview?url=https://somewhere.com/item.json&assets=B01&rescale=0,1000&colormap_name=cfastie` -### Crop / Part +### Bbox + -`:endpoint:/stac/crop/{minx},{miny},{maxx},{maxy}.{format}` +`:endpoint:/stac/bbox/{minx},{miny},{maxx},{maxy}.{format}` -`:endpoint:/stac/crop/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}` +`:endpoint:/stac/bbox/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}` - PathParams: - **minx,miny,maxx,maxy** (str): Comma (',') delimited bounding box in WGS84. - - **height** (int): Force output image height. **Optional** - - **width** (int): Force output image width. **Optional** - - **format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. **Optional** + - **format** (str): Output [image format](../user_guide/output_format.md). + - **height** (int, optional): Force output image height. **Also a QueryParam** + - **width** (int, optional): Force output image width. **Also a QueryParam** - QueryParams: - **url** (str): STAC Item URL. **Required** @@ -124,11 +136,13 @@ Example: - **expression** (str): rio-tiler's math expression with asset names (e.g `Asset1_b1/Asset2_b1`). - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset, so expression `Asset1/Asset2` can be passed. - **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`). - - **coord-crs** (str): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. - - **max_size** (int): Max image size, default is 1024. + - **coord_crs** (str): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. + - **dst_crs** (str): Output Coordinate Reference System. Default to `coord_crs`. + - **max_size** (int): Max image size. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. @@ -140,22 +154,29 @@ Example: !!! important - **assets** OR **expression** is required - - if **height** and **width** are provided **max_size** will be ignored. + - if **height** or **width** is provided **max_size** will be ignored. Example: -- `https://myendpoint/stac/crop/0,0,10,10.png?url=https://somewhere.com/item.json&assets=B01` -- `https://myendpoint/stac/crop/0,0,10,10.png?url=https://somewhere.com/item.json&assets=B01&rescale=0,1000&colormap_name=cfastie` +- `https://myendpoint/stac/bbox/0,0,10,10.png?url=https://somewhere.com/item.json&assets=B01` +- `https://myendpoint/stac/bbox/0,0,10,10/100x100.png?url=https://somewhere.com/item.json&assets=B01` +- `https://myendpoint/stac/bbox/0,0,10,10.png?url=https://somewhere.com/item.json&assets=B01&rescale=0,1000&colormap_name=cfastie` + +### Feature -`:endpoint:/stac/crop[/{width}x{height}][].{format}] - [POST]` +`:endpoint:/stac/feature - [POST]` + +`:endpoint:/stac/feature.{format} - [POST]` + +`:endpoint:/stac/feature/{width}x{height}.{format} - [POST]` - Body: - **feature** (JSON): A valid GeoJSON feature (Polygon or MultiPolygon) - PathParams: - - **height** (int): Force output image height. **Optional** - - **width** (int): Force output image width. **Optional** - - **format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. **Optional** + - **format** (str, optional): Output [image format](../user_guide/output_format.md). **Also a QueryParam** + - **height** (int, optional): Force output image height. **Also a QueryParam** + - **width** (int, optional): Force output image width. **Also a QueryParam** - QueryParams: - **url** (str): STAC Item URL. **Required** @@ -163,11 +184,13 @@ Example: - **expression** (str): rio-tiler's math expression with asset names (e.g `Asset1_b1/Asset2_b1`). - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset, so expression `Asset1/Asset2` can be passed. - **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`). - - **coord-crs** (str): Coordinate Reference System of the input geometry coordinates. Default to `epsg:4326`. - - **max_size** (int): Max image size, default is 1024. + - **coord_crs** (str): Coordinate Reference System of the input geometry coordinates. Default to `epsg:4326`. + - **dst_crs** (str): Output Coordinate Reference System. Default to `coord_crs`. + - **max_size** (int): Max image size. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. @@ -179,13 +202,13 @@ Example: !!! important - **assets** OR **expression** is required - - if **height** and **width** are provided **max_size** will be ignored. + - if **height** or **width** is provided **max_size** will be ignored. Example: -- `https://myendpoint/stac/crop?url=https://somewhere.com/item.json&assets=B01` -- `https://myendpoint/stac/crop.png?url=https://somewhere.com/item.json&assets=B01` -- `https://myendpoint/stac/crop/100x100.png?url=https://somewhere.com/item.json&assets=B01&rescale=0,1000&colormap_name=cfastie` +- `https://myendpoint/stac/feature?url=https://somewhere.com/item.json&assets=B01` +- `https://myendpoint/stac/feature.png?url=https://somewhere.com/item.json&assets=B01` +- `https://myendpoint/stac/feature/100x100.png?url=https://somewhere.com/item.json&assets=B01&rescale=0,1000&colormap_name=cfastie` ### Point @@ -200,10 +223,10 @@ Example: - **expression** (str): rio-tiler's math expression with asset names (e.g `Asset1_b1/Asset2_b1`). - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset, so expression `Asset1/Asset2` can be passed. - **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`). - - **coord-crs** (str): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. + - **coord_crs** (str): Coordinate Reference System of the input coordinates. Default to `epsg:4326`. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. !!! important **assets** OR **expression** is required @@ -214,10 +237,10 @@ Example: ### TilesJSON -`:endpoint:/stac[/{TileMatrixSetId}]/tilejson.json` tileJSON document +`:endpoint:/stac/{tileMatrixSetId}/tilejson.json` tileJSON document - PathParams: - - **TileMatrixSetId**: TileMatrixSet name, default is `WebMercatorQuad`. + - **tileMatrixSetId** (str): TileMatrixSet name (e.g `WebMercatorQuad`) - QueryParams: - **url** (str): STAC Item URL. **Required** @@ -225,19 +248,21 @@ Example: - **expression** (str): rio-tiler's math expression with asset names (e.g `Asset1_b1/Asset2_b1`). - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset, so expression `Asset1/Asset2` can be passed. - **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`). - - **tile_format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. + - **tile_format** (str): Output [image format](../user_guide/output_format.md), default is set to None and will be either JPEG or PNG depending on masked value. - **tile_scale** (int): Tile size scale, default is set to 1 (256x256). - **minzoom** (int): Overwrite default minzoom. - **maxzoom** (int): Overwrite default maxzoom. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. - **colormap_name** (str): rio-tiler color map name. - **return_mask** (bool): Add mask to the output data. Default is True. - - **buffer** (float): Add buffer on each side of the tile (e.g 0.5 = 257x257, 1.0 = 258x258). + - **buffer** (float): Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258). + - **padding** (int): Padding to apply to each tile edge. Helps reduce resampling artefacts along edges. Defaults to `0`. - **algorithm** (str): Custom algorithm name (e.g `hillshade`). - **algorithm_params** (str): JSON encoded algorithm parameters. @@ -246,16 +271,16 @@ Example: Example: -- `https://myendpoint/stac/tilejson.json?url=https://somewhere.com/item.json&assets=B01` -- `https://myendpoint/stac/tilejson.json?url=https://somewhere.com/item.json&assets=B01&tile_format=png` +- `https://myendpoint/stac/WebMercatorQuad/tilejson.json?url=https://somewhere.com/item.json&assets=B01` +- `https://myendpoint/stac/WebMercatorQuad/tilejson.json?url=https://somewhere.com/item.json&assets=B01&tile_format=png` - `https://myendpoint/stac/WorldCRS84Quad/tilejson.json?url=https://somewhere.com/item.json&tile_scale=2&expression=B01/B02` ### Map -`:endpoint:/stac[/{TileMatrixSetId}]/map` Simple viewer +`:endpoint:/stac/{tileMatrixSetId}/map.html` Simple viewer - PathParams: - - **TileMatrixSetId**: TileMatrixSet name, default is `WebMercatorQuad`. **Optional** + - **tileMatrixSetId** (str): TileMatrixSet name (e.g `WebMercatorQuad`) - QueryParams: - **url** (str): STAC Item URL. **Required** @@ -263,19 +288,21 @@ Example: - **expression** (str): rio-tiler's math expression with asset names (e.g `Asset1_b1/Asset2_b1`). - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset, so expression `Asset1/Asset2` can be passed. - **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`). - - **tile_format** (str): Output image format, default is set to None and will be either JPEG or PNG depending on masked value. + - **tile_format** (str): Output [image format](../user_guide/output_format.md), default is set to None and will be either JPEG or PNG depending on masked value. - **tile_scale** (int): Tile size scale, default is set to 1 (256x256). - **minzoom** (int): Overwrite default minzoom. - **maxzoom** (int): Overwrite default maxzoom. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. - **rescale** (array[str]): Comma (',') delimited Min,Max range (e.g `rescale=0,1000`, `rescale=0,1000&rescale=0,3000&rescale=0,2000`). - **color_formula** (str): rio-color formula. - **colormap** (str): JSON encoded custom Colormap. - **colormap_name** (str): rio-tiler color map name. - **return_mask** (bool): Add mask to the output data. Default is True. - - **buffer** (float): Add buffer on each side of the tile (e.g 0.5 = 257x257, 1.0 = 258x258). + - **buffer** (float): Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258). + - **padding** (int): Padding to apply to each tile edge. Helps reduce resampling artefacts along edges. Defaults to `0`. - **algorithm** (str): Custom algorithm name (e.g `hillshade`). - **algorithm_params** (str): JSON encoded algorithm parameters. @@ -284,8 +311,8 @@ Example: Example: -- `https://myendpoint/stac/tilejson.json?url=https://somewhere.com/item.json&assets=B01` -- `https://myendpoint/stac/tilejson.json?url=https://somewhere.com/item.json&assets=B01&tile_format=png` +- `https://myendpoint/stac/WebMercatorQuad/tilejson.json?url=https://somewhere.com/item.json&assets=B01` +- `https://myendpoint/stac/WebMercatorQuad/tilejson.json?url=https://somewhere.com/item.json&assets=B01&tile_format=png` - `https://myendpoint/stac/WorldCRS84Quad/tilejson.json?url=https://somewhere.com/item.json&tile_scale=2&expression=B01/B02` @@ -295,6 +322,7 @@ Example: - QueryParams: - **url** (str): STAC Item URL. **Required** + - **crs** (str): Geographic Coordinate Reference System. Default to `epsg:4326`. Example: @@ -318,11 +346,7 @@ Example: - QueryParams: - **url** (str): STAC Item URL. **Required** - **assets** (array[str]): asset names. Default to all available assets. - -Example: - -- `https://myendpoint/stac/info.geojson?url=https://somewhere.com/item.json&assets=B01` - + - **crs** (str): Geographic Coordinate Reference System. Default to `epsg:4326`. `:endpoint:/stac/assets` - Return the list of available assets @@ -344,7 +368,7 @@ Example: - **width** (int): Force image width from which to calculate statistics. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. - **categorical** (bool): Return statistics for categorical dataset, default is false. - **c** (array[float]): Pixels values for categories. - **p** (array[int]): Percentile values. @@ -369,7 +393,9 @@ Example: - **width** (int): Force image width from which to calculate statistics. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **algorithm** (str): Custom algorithm name (e.g `hillshade`). + - **algorithm_params** (str): JSON encoded algorithm parameters. - **categorical** (bool): Return statistics for categorical dataset, default is false. - **c** (array[float]): Pixels values for categories. - **p** (array[int]): Percentile values. @@ -392,13 +418,17 @@ Example: - **expression** (str): rio-tiler's math expression with asset names (e.g `Asset1_b1/Asset2_b1`). - **asset_as_band** (bool): tell rio-tiler that each asset is a 1 band dataset, so expression `Asset1/Asset2` can be passed. - **asset_bidx** (array[str]): Per asset band math expression (e.g `Asset1|1,2,3`). - - **coord-crs** (str): Coordinate Reference System of the input geometry coordinates. Default to `epsg:4326`. - - **max_size** (int): Max image size from which to calculate statistics, default is 1024. + - **coord_crs** (str): Coordinate Reference System of the input geometry coordinates. Default to `epsg:4326`. + - **dst_crs** (str): Output Coordinate Reference System. Default to `coord_crs`. + - **max_size** (int): Max image size from which to calculate statistics. - **height** (int): Force image height from which to calculate statistics. - **width** (int): Force image width from which to calculate statistics. - **nodata** (str, int, float): Overwrite internal Nodata value. - **unscale** (bool): Apply dataset internal Scale/Offset. - - **resampling** (str): rasterio resampling method. Default is `nearest`. + - **resampling** (str): RasterIO resampling algorithm. Defaults to `nearest`. + - **reproject** (str): WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`. + - **algorithm** (str): Custom algorithm name (e.g `hillshade`). + - **algorithm_params** (str): JSON encoded algorithm parameters. - **categorical** (bool): Return statistics for categorical dataset, default is false. - **c** (array[float]): Pixels values for categories. - **p** (array[int]): Percentile values. diff --git a/docs/src/endpoints/tms.md b/docs/src/endpoints/tms.md index 41cd57516..3a634b682 100644 --- a/docs/src/endpoints/tms.md +++ b/docs/src/endpoints/tms.md @@ -1,24 +1,14 @@ -The `titiler.application` package comes with a full FastAPI application with COG, STAC and MosaicJSON supports. -# TileMatrixSets - -The `tms` router extend the default `titiler.core.factory.TMSFactory`, adding some custom TileMatrixSets. - -```python -from fastapi import FastAPI -from titiler.application.routers.tms import tms - -app = FastAPI() -app.include_router(tms.router, tags=["TileMatrixSets"]) -``` +In addition to the `/cog`, `/stac` and `/mosaicjson` endpoints, the `titiler.application` package FastAPI application comes with additional metadata endpoints. +# TileMatrixSets ## API | Method | URL | Output | Description | ------ | ----------------------------------- |---------- |-------------- | `GET` | `/tileMatrixSets` | JSON | return the list of supported TileMatrixSet -| `GET` | `/tileMatrixSets/{TileMatrixSetId}` | JSON | return the TileMatrixSet JSON document +| `GET` | `/tileMatrixSets/{tileMatrixSetId}` | JSON | return the TileMatrixSet JSON document ## Description @@ -50,10 +40,10 @@ $ curl https://myendpoint/tileMatrixSets | jq ### Get TMS info -`:endpoint:/tileMatrixSets/{TileMatrixSetId}` - Get the TileMatrixSet JSON document +`:endpoint:/tileMatrixSets/{tileMatrixSetId}` - Get the TileMatrixSet JSON document - PathParams: - - **TileMatrixSetId**: TileMatrixSet name + - **tileMatrixSetId**: TileMatrixSet name ```bash $ curl http://127.0.0.1:8000/tileMatrixSets/WebMercatorQuad | jq diff --git a/docs/src/examples/code/create_gdal_wmts_extension.md b/docs/src/examples/code/create_gdal_wmts_extension.md index 4a687c84d..396127538 100644 --- a/docs/src/examples/code/create_gdal_wmts_extension.md +++ b/docs/src/examples/code/create_gdal_wmts_extension.md @@ -29,19 +29,7 @@ class gdalwmtsExtension(FactoryExtension): """Register endpoint to the tiler factory.""" @factory.router.get( - "/wmts.xml", - response_class=XMLResponse, - responses={ - 200: { - "description": "GDAL WMTS service description XML file", - "content": { - "application/xml": {}, - }, - }, - }, - ) - @factory.router.get( - "/{TileMatrixSetId}/wmts.xml", + "/{tileMatrixSetId}/wmts.xml", response_class=XMLResponse, responses={ 200: { @@ -54,9 +42,8 @@ class gdalwmtsExtension(FactoryExtension): ) def gdal_wmts( request: Request, - TileMatrixSetId: Literal[tuple(factory.supported_tms.list())] = Query( # type: ignore - factory.default_tms, - description=f"TileMatrixSet Name (default: '{factory.default_tms}')", + tileMatrixSetId: Literal[tuple(factory.supported_tms.list())] = Path( # type: ignore + description="TileMatrixSet Name", ), url: str = Depends(factory.path_dependency), # noqa bandscount: int = Query( @@ -74,7 +61,7 @@ class gdalwmtsExtension(FactoryExtension): ): """Return a GDAL WMTS Service description.""" route_params = { - "TileMatrixSetId": TileMatrixSetId, + "tileMatrixSetId": tileMatrixSetId, } wmts_url = factory.url_for(request, "wmts", **route_params) @@ -151,7 +138,7 @@ add_exception_handlers(app, DEFAULT_STATUS_CODES) ```python from rio_tiler.io import Reader -with Reader("http://0.0.0.0/wmts.xml?url=file.tif&bidx=1&bandscount=1&datatype=float32&tile_format=tif") as src: +with Reader("http://0.0.0.0/WebMercatorQuad/wmts.xml?url=file.tif&bidx=1&bandscount=1&datatype=float32&tile_format=tif") as src: im = src.preview() ``` diff --git a/docs/src/examples/code/img/example_custom_layers_docs.png b/docs/src/examples/code/img/example_custom_layers_docs.png new file mode 100644 index 000000000..b7176a650 Binary files /dev/null and b/docs/src/examples/code/img/example_custom_layers_docs.png differ diff --git a/docs/src/examples/code/img/example_custom_layers_preview.png b/docs/src/examples/code/img/example_custom_layers_preview.png new file mode 100644 index 000000000..f646d61c8 Binary files /dev/null and b/docs/src/examples/code/img/example_custom_layers_preview.png differ diff --git a/docs/src/examples/code/mosaic_from_urls.md b/docs/src/examples/code/mosaic_from_urls.md index 28f9e5913..f6239594d 100644 --- a/docs/src/examples/code/mosaic_from_urls.md +++ b/docs/src/examples/code/mosaic_from_urls.md @@ -105,7 +105,7 @@ class MultiFilesBackend(BaseBackend): ```python """routes. -app/router.py +app/routers.py """ @@ -136,7 +136,7 @@ def DatasetPathParams(url: str = Query(..., description="Dataset URL")) -> List[ return url.split(",") -mosaic = MosaicTiler(reader=MultiFilesBackend, path_dependency=DatasetPathParams) +mosaic = MosaicTiler(backend=MultiFilesBackend, path_dependency=DatasetPathParams) ``` diff --git a/docs/src/examples/code/tiler_for_sentinel2.md b/docs/src/examples/code/tiler_for_sentinel2.md index d0e0256c3..c867e0a76 100644 --- a/docs/src/examples/code/tiler_for_sentinel2.md +++ b/docs/src/examples/code/tiler_for_sentinel2.md @@ -165,7 +165,7 @@ dates = [f["properties"]["datetime"][0:10] for f in data["features"]] # Fetch TileJSON # For this example we use the first `sceneid` returned from the STAC API # and we sent the Bands to B04,B03,B02 which are red,green,blue -data = httpx.get(f"{titiler_endpoint}/scenes/tilejson.json?sceneid={sceneid[4]}&bands=B04&bands=B03&bands=B02&rescale=0,2000").json() +data = httpx.get(f"{titiler_endpoint}/scenes/WebMercatorQuad/tilejson.json?sceneid={sceneid[4]}&bands=B04&bands=B03&bands=B02&rescale=0,2000").json() print(data) ``` @@ -239,6 +239,6 @@ with MosaicBackend(mosaic_file, mosaic_def=mosaic_doc) as mosaic: Use the mosaic in titiler ```python mosaic = str(pathlib.Path(mosaic_file).absolute()) -data = httpx.get(f"{titiler_endpoint}/mosaic/tilejson.json?url=file:///{mosaic}&bands=B01&rescale=0,1000").json() +data = httpx.get(f"{titiler_endpoint}/mosaic/WebMercatorQuad/tilejson.json?url=file:///{mosaic}&bands=B01&rescale=0,1000").json() print(data) ``` diff --git a/docs/src/examples/code/tiler_with_auth.md b/docs/src/examples/code/tiler_with_auth.md index b45acf690..801499d42 100644 --- a/docs/src/examples/code/tiler_with_auth.md +++ b/docs/src/examples/code/tiler_with_auth.md @@ -43,7 +43,7 @@ app/models.py """ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import List, Optional from jose import jwt @@ -67,7 +67,7 @@ class AccessToken(BaseModel): @validator("iat", pre=True, always=True) def set_creation_time(cls, v) -> datetime: """Set token creation time (iat).""" - return datetime.utcnow() + return datetime.now(timezone.UTC) @validator("exp", always=True) def set_expiration_time(cls, v, values) -> datetime: @@ -140,12 +140,12 @@ def DatasetPathParams( """Create dataset path from args""" if not api_key_query: - raise HTTPException(status_code=403, detail="Missing `access_token`") + raise HTTPException(status_code=401, detail="Missing `access_token`") try: AccessToken.from_string(api_key_query) except JWTError: - raise HTTPException(status_code=403, detail="Invalid `access_token`") + raise HTTPException(status_code=401, detail="Invalid `access_token`") return url ``` diff --git a/docs/src/examples/code/tiler_with_cache.md b/docs/src/examples/code/tiler_with_cache.md index 880ed6875..a29d7bd82 100644 --- a/docs/src/examples/code/tiler_with_cache.md +++ b/docs/src/examples/code/tiler_with_cache.md @@ -167,7 +167,7 @@ from rio_tiler.io import BaseReader, Reader from titiler.core.factory import img_endpoint_params from titiler.core.factory import TilerFactory as TiTilerFactory -from titiler.core.dependencies import ImageParams, RescalingParams +from titiler.core.dependencies import RescalingParams from titiler.core.models.mapbox import TileJSON from titiler.core.resources.enums import ImageType @@ -186,15 +186,15 @@ class TilerFactory(TiTilerFactory): @self.router.get(r"/tiles/{z}/{x}/{y}.{format}", **img_endpoint_params) @self.router.get(r"/tiles/{z}/{x}/{y}@{scale}x", **img_endpoint_params) @self.router.get(r"/tiles/{z}/{x}/{y}@{scale}x.{format}", **img_endpoint_params) - @self.router.get(r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}", **img_endpoint_params) + @self.router.get(r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}", **img_endpoint_params) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}.{format}", **img_endpoint_params + r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}.{format}", **img_endpoint_params ) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}@{scale}x", **img_endpoint_params + r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}@{scale}x", **img_endpoint_params ) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}@{scale}x.{format}", + r"/tiles/{tileMatrixSetId}/{z}/{x}/{y}@{scale}x.{format}", **img_endpoint_params, ) # Add default cache config dictionary into cached alias. @@ -204,7 +204,7 @@ class TilerFactory(TiTilerFactory): z: int = Path(..., ge=0, le=30, description="TMS tiles's zoom level"), x: int = Path(..., description="TMS tiles's column"), y: int = Path(..., description="TMS tiles's row"), - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( + tileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( self.default_tms, description=f"TileMatrixSet Name (default: '{self.default_tms}')", ), @@ -235,7 +235,7 @@ class TilerFactory(TiTilerFactory): reader_params=Depends(self.reader_dependency), ): """Create map tile from a dataset.""" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) with self.reader(src_path, tms=tms, **reader_params) as src_dst: image = src_dst.tile( @@ -280,7 +280,7 @@ class TilerFactory(TiTilerFactory): response_model_exclude_none=True, ) @self.router.get( - "/{TileMatrixSetId}/tilejson.json", + "/{tileMatrixSetId}/tilejson.json", response_model=TileJSON, responses={200: {"description": "Return a tilejson"}}, response_model_exclude_none=True, @@ -288,7 +288,7 @@ class TilerFactory(TiTilerFactory): @cached(alias="default") def tilejson( request: Request, - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( + tileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( self.default_tms, description=f"TileMatrixSet Name (default: '{self.default_tms}')", ), @@ -332,7 +332,7 @@ class TilerFactory(TiTilerFactory): "x": "{x}", "y": "{y}", "scale": tile_scale, - "TileMatrixSetId": TileMatrixSetId, + "tileMatrixSetId": tileMatrixSetId, } if tile_format: route_params["format"] = tile_format.value @@ -354,7 +354,7 @@ class TilerFactory(TiTilerFactory): if qs: tiles_url += f"?{urlencode(qs)}" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) with self.reader(src_path, tms=tms, **reader_params) as src_dst: return { "bounds": src_dst.geographic_bounds, diff --git a/docs/src/examples/code/tiler_with_custom_algorithm.md b/docs/src/examples/code/tiler_with_custom_algorithm.md index e606982e6..547d5ffe5 100644 --- a/docs/src/examples/code/tiler_with_custom_algorithm.md +++ b/docs/src/examples/code/tiler_with_custom_algorithm.md @@ -32,7 +32,6 @@ class Multiply(BaseAlgorithm): # Create output ImageData return ImageData( data, - img.mask, assets=img.assets, crs=img.crs, bounds=img.bounds, diff --git a/docs/src/examples/code/tiler_with_custom_colormap.md b/docs/src/examples/code/tiler_with_custom_colormap.md index 8a9e5f86e..fd88ecc7b 100644 --- a/docs/src/examples/code/tiler_with_custom_colormap.md +++ b/docs/src/examples/code/tiler_with_custom_colormap.md @@ -15,7 +15,7 @@ cmap = urlencode( } ) response = requests.get( - f"http://127.0.0.1:8000/cog/tiles/8/53/50.png?url=https://myurl.com/cog.tif&bidx=1&rescale=0,10000&{cmap}" + f"http://127.0.0.1:8000/cog/tiles/WebMercatorQuad/8/53/50.png?url=https://myurl.com/cog.tif&bidx=1&rescale=0,10000&{cmap}" ) ``` @@ -33,34 +33,34 @@ app/dependencies.py """ import json -from enum import Enum -from typing import Dict, Optional + +from typing import Dict, Optional, Literal +from typing_extensions import Annotated import numpy import matplotlib -from rio_tiler.colormap import cmap, parse_color +from rio_tiler.colormap import parse_color +from rio_tiler.colormap import cmap as default_cmap from fastapi import HTTPException, Query -ColorMapName = Enum( # type: ignore - "ColorMapName", [(a, a) for a in sorted(cmap.list())] -) - -class ColorMapType(str, Enum): - """Colormap types.""" - - explicit = "explicit" - linear = "linear" - - def ColorMapParams( - colormap_name: ColorMapName = Query(None, description="Colormap name"), - colormap: str = Query(None, description="JSON encoded custom Colormap"), - colormap_type: ColorMapType = Query(ColorMapType.explicit, description="User input colormap type."), + colormap_name: Annotated[ # type: ignore + Literal[tuple(default_cmap.list())], + Query(description="Colormap name"), + ] = None, + colormap: Annotated[ + str, + Query(description="JSON encoded custom Colormap"), + ] = None, + colormap_type: Annotated[ + Literal["explicit", "linear"], + Query(description="User input colormap type."), + ] = "explicit", ) -> Optional[Dict]: """Colormap Dependency.""" if colormap_name: - return cmap.get(colormap_name.value) + return default_cmap.get(colormap_name) if colormap: try: @@ -73,7 +73,7 @@ def ColorMapParams( status_code=400, detail="Could not parse the colormap value." ) - if colormap_type == ColorMapType.linear: + if colormap_type == "linear": # input colormap has to start from 0 to 255 ? cm = matplotlib.colors.LinearSegmentedColormap.from_list( 'custom', diff --git a/docs/src/examples/code/tiler_with_custom_stac+xarray.md b/docs/src/examples/code/tiler_with_custom_stac+xarray.md new file mode 100644 index 000000000..4b807091d --- /dev/null +++ b/docs/src/examples/code/tiler_with_custom_stac+xarray.md @@ -0,0 +1,380 @@ + +**Goal**: Create a custom STAC Reader supporting both COG and NetCDF/Zarr dataset + +**requirements**: + +- `titiler.core` +- `titiler.xarray` +- `fsspec` +- `zarr` +- `h5netcdf` +- `aiohttp` (optional) +- `s3fs` (optional) + +**links**: + +- https://cogeotiff.github.io/rio-tiler/examples/STAC_datacube_support/ + + +#### 1. Custom STACReader + +First, we need to create a custom `STACReader` which will support both COG and NetCDF/Zarr dataset. The custom parts will be: + +- add `netcdf` and `zarr` as valid asset media types +- introduce a new `md://` prefixed asset form, so users can pass `assets=md://{netcdf asset name}?variable={variable name}` as we do for the `GDAL vrt string connection` support. + +```python title="stac.py" +from typing import Set, Type, Tuple, Dict, Optional + +import attr +from urllib.parse import urlparse, parse_qsl +from rio_tiler.types import AssetInfo +from rio_tiler.io import BaseReader, Reader +from rio_tiler.io import stac + +from titiler.xarray.io import Reader as XarrayReader + +valid_types = { + *stac.DEFAULT_VALID_TYPE, + "application/x-netcdf", + "application/vnd+zarr", +} + + +@attr.s +class STACReader(stac.STACReader): + """Custom STACReader which adds support for `md://` prefixed assets. + + Example: + >>> with STACReader("https://raw.githubusercontent.com/cogeotiff/rio-tiler/refs/heads/main/tests/fixtures/stac_netcdf.json") as src: + print(src.assets) + print(src._get_asset_info("md://netcdf?variable=dataset")) + + ['geotiff', 'netcdf'] + {'url': 'https://raw.githubusercontent.com/cogeotiff/rio-tiler/refs/heads/main/tests/fixtures/dataset_2d.nc', 'metadata': {}, 'reader_options': {'variable': 'dataset'}, 'media_type': 'application/x-netcdf'} + + """ + include_asset_types: Set[str] = attr.ib(default=valid_types) + + def _get_reader(self, asset_info: AssetInfo) -> Tuple[Type[BaseReader], Dict]: + """Get Asset Reader.""" + asset_type = asset_info.get("media_type", None) + if asset_type and asset_type in [ + "application/x-netcdf", + "application/vnd+zarr", + "application/x-hdf5", + "application/x-hdf", + ]: + return XarrayReader, asset_info.get("reader_options", {}) + + return Reader, asset_info.get("reader_options", {}) + + def _parse_md_asset(self, asset: str) -> Tuple[str, Optional[Dict]]: + """Parse md:// asset string and return both asset name and reader options""" + if asset.startswith("md://") and asset not in self.assets: + parsed = urlparse(asset) + if not parsed.netloc or parsed.netloc not in self.assets: + raise InvalidAssetName( + f"'{parsed.netloc}' is not valid, should be one of {self.assets}" + ) + + # NOTE: by using `parse_qsl` we assume the + # reader_options are in form of `key=single_value` + # reader_options for XarrayReader are: + # - variable: str + # - group: Optional[str] + # - decode_times: bool = True + # - datetime: Optional[str] + # - drop_dim: Optional[str] + return parsed.netloc, dict(parse_qsl(parsed.query)) + + return asset, None + + def _get_asset_info(self, asset: str) -> AssetInfo: + """Validate asset names and return asset's info. + + Args: + asset (str): STAC asset name. + + Returns: + AssetInfo: STAC asset info. + + """ + vrt_options = None + reader_options = None + if asset.startswith("vrt://"): + asset, vrt_options = self._parse_vrt_asset(asset) + + # not part of the original STACReader + elif asset.startswith("md://"): + asset, reader_options = self._parse_md_asset(asset) + + if asset not in self.assets: + raise InvalidAssetName( + f"'{asset}' is not valid, should be one of {self.assets}" + ) + + asset_info = self.item.assets[asset] + extras = asset_info.extra_fields + + info = AssetInfo( + url=asset_info.get_absolute_href() or asset_info.href, + metadata=extras if not vrt_options else None, + reader_options=reader_options or {} + ) + + if stac.STAC_ALTERNATE_KEY and extras.get("alternate"): + if alternate := extras["alternate"].get(stac.STAC_ALTERNATE_KEY): + info["url"] = alternate["href"] + + if asset_info.media_type: + info["media_type"] = asset_info.media_type + + # https://github.com/stac-extensions/file + if head := extras.get("file:header_size"): + info["env"] = {"GDAL_INGESTED_BYTES_AT_OPEN": head} + + # https://github.com/stac-extensions/raster + if extras.get("raster:bands") and not vrt_options: + bands = extras.get("raster:bands") + stats = [ + (b["statistics"]["minimum"], b["statistics"]["maximum"]) + for b in bands + if {"minimum", "maximum"}.issubset(b.get("statistics", {})) + ] + # check that stats data are all double and make warning if not + if ( + stats + and all(isinstance(v, (int, float)) for stat in stats for v in stat) + and len(stats) == len(bands) + ): + info["dataset_statistics"] = stats + else: + warnings.warn( + "Some statistics data in STAC are invalid, they will be ignored." + ) + + if vrt_options: + info["url"] = f"vrt://{info['url']}?{vrt_options}" + + return info +``` + +#### 2. Endpoint Factory + +Custom `MultiBaseTilerFactory` which removes some endpoints (`/preview`) and adapt dependencies to work with both COG and Xarray Datasets. + +```python title="factory.py" +"""Custom MultiBaseTilerFactory.""" +from dataclasses import dataclass +from typing import Type, Union, Optional, List +from typing_extensions import Annotated +from attrs import define, field +from geojson_pydantic.features import Feature, FeatureCollection +from fastapi import Body, Depends, Query +from titiler.core import factory +from titiler.core.dependencies import ( + DefaultDependency, + BidxParams, + AssetsParams, + AssetsBidxExprParamsOptional, + CoordCRSParams, + DstCRSParams, +) +from titiler.core.models.responses import MultiBaseStatisticsGeoJSON +from titiler.core.resources.responses import GeoJSONResponse +from rio_tiler.constants import WGS84_CRS +from rio_tiler.io import MultiBaseReader + +from stac import STACReader + + +# Simple Asset dependency (1 asset, no expression) +@dataclass +class SingleAssetsParams(DefaultDependency): + """Custom Assets parameters which only accept ONE asset and make it required.""" + + assets: Annotated[ + str, + Query(title="Asset names", description="Asset's name."), + ] + + indexes: Annotated[ + Optional[List[int]], + Query( + title="Band indexes", + alias="bidx", + description="Dataset band indexes", + openapi_examples={ + "user-provided": {"value": None}, + "one-band": {"value": [1]}, + "multi-bands": {"value": [1, 2, 3]}, + }, + ), + ] = None + + +@define(kw_only=True) +class MultiBaseTilerFactory(factory.MultiBaseTilerFactory): + + reader: Type[MultiBaseReader] = STACReader + + # Assets/Indexes/Expression dependency + layer_dependency: Type[DefaultDependency] = SingleAssetsParams + + # Assets dependency (for /info endpoints) + assets_dependency: Type[DefaultDependency] = AssetsParams + + # remove preview endpoints + img_preview_dependency: Type[DefaultDependency] = field(init=False) + add_preview: bool = field(init=False, default=False) + + # Overwrite the `/statistics` endpoint to remove `full` dataset statistics (which could be unusable for NetCDF dataset) + def statistics(self): # noqa: C901 + """Register /statistics endpoint.""" + + @self.router.post( + "/statistics", + response_model=MultiBaseStatisticsGeoJSON, + response_model_exclude_none=True, + response_class=GeoJSONResponse, + responses={ + 200: { + "content": {"application/geo+json": {}}, + "description": "Return dataset's statistics from feature or featureCollection.", + } + }, + ) + def geojson_statistics( + geojson: Annotated[ + Union[FeatureCollection, Feature], + Body(description="GeoJSON Feature or FeatureCollection."), + ], + src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), + layer_params=Depends(AssetsBidxExprParamsOptional), + dataset_params=Depends(self.dataset_dependency), + coord_crs=Depends(CoordCRSParams), + dst_crs=Depends(DstCRSParams), + post_process=Depends(self.process_dependency), + image_params=Depends(self.img_part_dependency), + stats_params=Depends(self.stats_dependency), + histogram_params=Depends(self.histogram_dependency), + env=Depends(self.environment_dependency), + ): + """Get Statistics from a geojson feature or featureCollection.""" + fc = geojson + if isinstance(fc, Feature): + fc = FeatureCollection(type="FeatureCollection", features=[geojson]) + + with rasterio.Env(**env): + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + # Default to all available assets + if not layer_params.assets and not layer_params.expression: + layer_params.assets = src_dst.assets + + for feature in fc: + image = src_dst.feature( + feature.model_dump(exclude_none=True), + shape_crs=coord_crs or WGS84_CRS, + dst_crs=dst_crs, + align_bounds_with_dataset=True, + **layer_params.as_dict(), + **image_params.as_dict(), + **dataset_params.as_dict(), + ) + + if post_process: + image = post_process(image) + + stats = image.statistics( + **stats_params.as_dict(), + hist_options=histogram_params.as_dict(), + ) + + feature.properties = feature.properties or {} + # NOTE: because we use `src_dst.feature` the statistics will be in form of + # `Dict[str, BandStatistics]` and not `Dict[str, Dict[str, BandStatistics]]` + feature.properties.update({"statistics": stats}) + + return fc.features[0] if isinstance(geojson, Feature) else fc +``` + +#### 3. Application + +```python title="main.py" +"""FastAPI application.""" + +from fastapi import FastAPI + +from titiler.core.dependencies import DatasetPathParams +from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers + +from factory import MultiBaseTilerFactory + +# STAC uses MultiBaseReader so we use MultiBaseTilerFactory to built the default endpoints +stac = MultiBaseTilerFactory(router_prefix="stac") + +# Create FastAPI application +app = FastAPI() +app.include_router(stac.router, tags=["STAC"]) +add_exception_handlers(app, DEFAULT_STATUS_CODES) +``` + +``` +uvicorn app:app --port 8080 --reload +``` + +Screenshot 2024-11-07 at 4 42 21 PM + + +```bash +curl http://127.0.0.1:8080/assets\?url\=https%3A%2F%2Fraw.githubusercontent.com%2Fcogeotiff%2Frio-tiler%2Frefs%2Fheads%2Fmain%2Ftests%2Ffixtures%2Fstac_netcdf.json | jq + +[ + "geotiff", + "netcdf" +] +``` + +```bash +curl http://127.0.0.1:8080/info?url=https://raw.githubusercontent.com/cogeotiff/rio-tiler/refs/heads/main/tests/fixtures/stac_netcdf.json&assets=md://netcdf?variable=dataset | jq +{ + "md://netcdf?variable=dataset": { + "bounds": [ + -170.085, + -80.08, + 169.914999999975, + 79.91999999999659 + ], + "crs": "http://www.opengis.net/def/crs/EPSG/0/4326", + "band_metadata": [ + [ + "b1", + {} + ] + ], + "band_descriptions": [ + [ + "b1", + "value" + ] + ], + "dtype": "float64", + "nodata_type": "Nodata", + "name": "dataset", + "count": 1, + "width": 2000, + "height": 1000, + "attrs": { + "valid_min": 1.0, + "valid_max": 1000.0, + "fill_value": 0 + } + } +} +``` + +```bash +curl http://127.0.0.1:8080/tiles/WebMercatorQuad/1/0/0?url=https://raw.githubusercontent.com/cogeotiff/rio-tiler/refs/heads/main/tests/fixtures/stac_netcdf.json&assets=md://netcdf?variable=dataset&rescale=0,1000 +``` diff --git a/docs/src/examples/code/tiler_with_custom_tms.md b/docs/src/examples/code/tiler_with_custom_tms.md index eb4edacc1..0813dee9c 100644 --- a/docs/src/examples/code/tiler_with_custom_tms.md +++ b/docs/src/examples/code/tiler_with_custom_tms.md @@ -20,15 +20,14 @@ from pyproj import CRS EPSG6933 = TileMatrixSet.custom( (-17357881.81713629, -7324184.56362408, 17357881.81713629, 7324184.56362408), CRS.from_epsg(6933), - identifier="EPSG6933", + id="EPSG6933", matrix_scale=[1, 1], ) - # 2. Register TMS -tms = tms.register([EPSG6933]) +tms = tms.register({EPSG6933.id:EPSG6933}) -tms = TMSFactory(supported_tms=tms) -cog = TilerFactory(supported_tms=tms) +tms_factory = TMSFactory(supported_tms=tms) +cog_factory = TilerFactory(supported_tms=tms) ``` 2 - Create app and register our custom endpoints @@ -44,11 +43,11 @@ from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers from fastapi import FastAPI -from .routes import cog, tms +from .routes import cog_factory, tms_factory app = FastAPI(title="My simple app with custom TMS") -app.include_router(cog.router, tags=["Cloud Optimized GeoTIFF"]) -app.include_router(tms.router, tags=["Tiling Schemes"]) +app.include_router(cog_factory.router, tags=["Cloud Optimized GeoTIFF"]) +app.include_router(tms_factory.router, tags=["Tiling Schemes"]) add_exception_handlers(app, DEFAULT_STATUS_CODES) ``` diff --git a/docs/src/examples/code/tiler_with_layers.md b/docs/src/examples/code/tiler_with_layers.md new file mode 100644 index 000000000..1465ba05d --- /dev/null +++ b/docs/src/examples/code/tiler_with_layers.md @@ -0,0 +1,243 @@ + +**Goal**: Create a Custom TiTiler with a limited set of supported `layers` + +**requirements**: titiler.core + +**How**: + +The idea is to create a set of endpoints with a `/layers/{layer_id}` prefix and a set of configuration, e.g + +``` +config = { + "layer_1": { + "url": "dataset_1 url", + "indexes": [1], + "render": { + "rescale": [(0, 1000)], + "colormap_name": "viridis" + } + }, + ... +} +``` + +We then use custom set of endpoint dependencies to get the `layer` configuration and `inject` the parameters. + + +```python +import json +from dataclasses import dataclass, field +from typing import Dict, Literal, Annotated, Optional, Sequence + +from fastapi import FastAPI, Path, HTTPException, Query +from rio_tiler.colormap import ColorMaps +from rio_tiler.colormap import cmap as default_cmap +from rio_tiler.colormap import parse_color +from starlette.requests import Request + +from titiler.core import dependencies +from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers +from titiler.core.factory import TilerFactory + + +# Layers Configuration +available_layers = { + "red": { + "url": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/21/J/XN/2021/2/S2B_21JXN_20210214_1_L2A/B04.tif", + "render": { + "rescale": [ + (0, 1000), + ], + }, + }, + "green": { + "url": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/21/J/XN/2021/2/S2B_21JXN_20210214_1_L2A/B03.tif", + "render": { + "rescale": [ + (0, 1000), + ], + }, + }, + "bleue": { + "url": "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/21/J/XN/2021/2/S2B_21JXN_20210214_1_L2A/B02.tif", + "render": { + "rescale": [ + (0, 1000), + ], + }, + }, +} + +# VRT of bands B04, B03, B02, and B05 files +# gdalbuildvrt vrt.vrt /vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/21/J/XN/2021/2/S2B_21JXN_20210214_1_L2A/B{04,03,02,08}.tif -separate +# cat vrt.vrt | tr -d '\n' | tr -d ' ' +vrt_rdbnir = ' PROJCS["WGS 84 / UTM zone 21S",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],PARAMETER["central_meridian",-57],PARAMETER["scale_factor",0.9996],PARAMETER["false_easting",500000],PARAMETER["false_northing",10000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","32721"]] 6.0000000000000000e+05, 1.0000000000000000e+01, 0.0000000000000000e+00, 7.3000000000000000e+06, 0.0000000000000000e+00, -1.0000000000000000e+01 0 /vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/21/J/XN/2021/2/S2B_21JXN_20210214_1_L2A/B04.tif 1 0 0 /vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/21/J/XN/2021/2/S2B_21JXN_20210214_1_L2A/B03.tif 1 0 0 /vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/21/J/XN/2021/2/S2B_21JXN_20210214_1_L2A/B02.tif 1 0 0 /vsicurl/https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/21/J/XN/2021/2/S2B_21JXN_20210214_1_L2A/B08.tif 1 0 ' + +# Mpre configs (using VRT) +available_layers.update( + { + "true_color": { + "url": vrt_rdbnir, + "indexes": [1, 2, 3], + "render": { + "rescale": [ + (0, 3000), + (0, 3000), + (0, 3000), + ], + }, + }, + "false_color": { + "url": vrt_rdbnir, + "indexes": [4, 1, 2], + "render": { + "rescale": [ + (0, 4000), + (0, 3000), + (0, 3000), + ], + }, + }, + "ndvi": { + "url": vrt_rdbnir, + "expression": "(b4-b1)/(b4+b1)", + "render": { + "rescale": [ + (-1, 1), + ], + "colormap_name": "viridis", + }, + }, + } +) + +# List of all Layers +layers_list = Literal["red", "green", "blue", "true_color", "false_color", "ndvi"] + + +# Custom `DatasetPathParams` which return the dataset URL for a `layer_id` +def DatasetPathParams(layer_id: layers_list = Path()) -> str: + return available_layers[layer_id]["url"] + + +@dataclass +class CustomAsDict: + """Custom `DefaultDependency` to ignore `requests`""" + def as_dict(self, exclude_none: bool = True) -> Dict: + """Transform dataclass to dict.""" + exclude_keys = {"request"} + if exclude_none: + return { + k: v + for k, v in self.__dict__.items() + if v is not None and k not in exclude_keys + } + + return {k: v for k, v in self.__dict__.items() if k not in exclude_keys} + + +# Custom Layer Param +@dataclass +class LayerParams(CustomAsDict, dependencies.BidxExprParams): + + request: Request = field(default=None) + + def __post_init__(self): + if (layer := self.request.path_params.get("layer_id")) and not any( + [self.indexes, self.expression] + ): + layer_params = available_layers[layer] + if indexes := layer_params.get("indexes"): + self.indexes = indexes + elif expr := layer_params.get("expression"): + self.expression = expr + +# Custom Rendering Params +@dataclass +class RenderingParams(CustomAsDict, dependencies.ImageRenderingParams): + + request: Request = field(default=None) + + def __post_init__(self): + super().__post_init__() + + if layer := self.request.path_params.get("layer_id"): + layer_params = available_layers[layer].get("render", {}) + + if not self.rescale and (rescale := layer_params.get("rescale")): + self.rescale = rescale + + if not self.color_formula and (color_formula := layer_params.get("color_formula")): + self.color_formula = color_formula + + if self.add_mask is not None and (add_mask := layer_params.get("add_mask")): + self.add_mask = add_mask + + +# Custom ColorMap Params +def ColorMapParams( + request: Request, + colormap_name: Annotated[ # type: ignore + Literal[tuple(default_cmap.list())], + Query(description="Colormap name"), + ] = None, + colormap: Annotated[ + Optional[str], Query(description="JSON encoded custom Colormap") + ] = None, +): + if layer := request.path_params.get("layer_id"): + layer_params = available_layers[layer].get("render", {}) + colormap_name = layer_params.get("colormap_name", colormap_name) + colormap = layer_params.get("colormap", colormap) + + if colormap_name: + return default_cmap.get(colormap_name) + + if colormap: + try: + c = json.loads( + colormap, + object_hook=lambda x: { + int(k): parse_color(v) for k, v in x.items() + }, + ) + + # Make sure to match colormap type + if isinstance(c, Sequence): + c = [(tuple(inter), parse_color(v)) for (inter, v) in c] + + return c + except json.JSONDecodeError as e: + raise HTTPException( + status_code=400, detail="Could not parse the colormap value." + ) from e + + return None + + +app = FastAPI() + +cog = TilerFactory( + path_dependency=DatasetPathParams, + layer_dependency=LayerParams, + render_dependency=RenderingParams, + colormap_dependency=ColorMapParams, + router_prefix="/layers/{layer_id}", +) +app.include_router(cog.router, prefix="/layers/{layer_id}") +add_exception_handlers(app, DEFAULT_STATUS_CODES) + +# Run the application +import uvicorn +uvicorn.run(app=app, host="127.0.0.1", port=8080, log_level="info") +``` + + +`http://127.0.0.1:8080/docs` + +![](img/example_custom_layers_docs.png) + + +`http://127.0.0.1:8080/layers/true_color/preview` + +![](img/example_custom_layers_preview.png) diff --git a/docs/src/examples/notebooks/Working_with_Algorithm.ipynb b/docs/src/examples/notebooks/Working_with_Algorithm.ipynb index cb0fe237c..19233ce51 100644 --- a/docs/src/examples/notebooks/Working_with_Algorithm.ipynb +++ b/docs/src/examples/notebooks/Working_with_Algorithm.ipynb @@ -21,7 +21,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": null, "id": "55915667", "metadata": {}, "outputs": [], @@ -34,17 +34,19 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "3ac532e8", "metadata": {}, "outputs": [], "source": [ - "titiler_endpoint = \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind." + "titiler_endpoint = (\n", + " \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + ")" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "5c65b3d5", "metadata": {}, "outputs": [], @@ -62,25 +64,17 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "7abeb9c0", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'bounds': [7.090624928537461, 45.916058441028206, 7.1035698381384185, 45.925093000254144], 'minzoom': 15, 'maxzoom': 18, 'band_metadata': [['b1', {'STATISTICS_COVARIANCES': '10685.98787505646', 'STATISTICS_EXCLUDEDVALUES': '-9999', 'STATISTICS_MAXIMUM': '2015.0944824219', 'STATISTICS_MEAN': '1754.471184271', 'STATISTICS_MINIMUM': '1615.8128662109', 'STATISTICS_SKIPFACTORX': '1', 'STATISTICS_SKIPFACTORY': '1', 'STATISTICS_STDDEV': '103.37305197708'}]], 'band_descriptions': [['b1', '']], 'dtype': 'float32', 'nodata_type': 'Nodata', 'colorinterp': ['gray'], 'width': 2000, 'nodata_value': -9999.0, 'overviews': [2, 4, 8], 'count': 1, 'height': 2000, 'driver': 'GTiff'}\n" - ] - } - ], + "outputs": [], "source": [ "# Fetch dataset Metadata\n", "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/info\",\n", - " params = {\n", + " params={\n", " \"url\": url,\n", - " }\n", + " },\n", ").json()\n", "\n", "print(r)" @@ -98,226 +92,57 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "80803e00", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": url,\n", - " }\n", + " },\n", ").json()\n", "\n", "bounds = r[\"bounds\"]\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=r[\"minzoom\"]\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=r[\"minzoom\"],\n", ")\n", "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"Office fédéral de topographie swisstopo\"\n", - ")\n", - "aod_layer.add_to(m)\n", + "TileLayer(\n", + " tiles=r[\"tiles\"][0], opacity=1, attr=\"Office fédéral de topographie swisstopo\"\n", + ").add_to(m)\n", "m" ] }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "id": "64c2faab", "metadata": { "scrolled": false }, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": url,\n", " # rio-tiler cannot rescale automatically the data when using a colormap\n", " \"rescale\": \"1615.812,2015.09448\",\n", " \"colormap_name\": \"terrain\",\n", - " }\n", + " },\n", ").json()\n", "\n", "bounds = r[\"bounds\"]\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=r[\"minzoom\"]\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=r[\"minzoom\"],\n", ")\n", "\n", "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"Office fédéral de topographie swisstopo\"\n", + " tiles=r[\"tiles\"][0], opacity=1, attr=\"Office fédéral de topographie swisstopo\"\n", ")\n", "aod_layer.add_to(m)\n", "m" @@ -333,29 +158,12 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": null, "id": "26ef9eef", "metadata": { "scrolled": true }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Available algorithm\n", - "['hillshade', 'contours', 'normalizedIndex', 'terrarium', 'terrainrgb']\n", - "\n", - "Metadata from `Hillshade` algorithm\n", - "Inputs\n", - "{'nbands': 1}\n", - "Outputs\n", - "{'nbands': 1, 'dtype': 'uint8'}\n", - "Parameters\n", - "{'azimuth': {'title': 'Azimuth', 'default': 90, 'type': 'integer'}, 'angle_altitude': {'title': 'Angle Altitude', 'default': 90, 'type': 'number'}, 'buffer': {'title': 'Buffer', 'default': 3, 'type': 'integer'}}\n" - ] - } - ], + "outputs": [], "source": [ "# Fetch algorithms\n", "print(\"Available algorithm\")\n", @@ -381,117 +189,31 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "id": "4cc8c900", "metadata": { "scrolled": false }, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": url,\n", " \"algorithm\": \"hillshade\",\n", - " # Hillshade algorithm use a 3pixel buffer so we need \n", + " # Hillshade algorithm use a 3pixel buffer so we need\n", " # to tell the tiler to apply a 3 pixel buffer around each tile\n", " \"buffer\": 3,\n", - " }\n", + " },\n", ").json()\n", "\n", "bounds = r[\"bounds\"]\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=r[\"minzoom\"]\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=r[\"minzoom\"],\n", ")\n", "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"Yo!!\"\n", - ")\n", + "aod_layer = TileLayer(tiles=r[\"tiles\"][0], opacity=1, attr=\"Yo!!\")\n", "aod_layer.add_to(m)\n", "m" ] @@ -506,241 +228,67 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": null, "id": "54d674e9", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 36, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": url,\n", " \"algorithm\": \"contours\",\n", " \"algorithm_params\": json.dumps(\n", " {\n", - " \"increment\": 20, # contour line every 20 meters\n", - " \"thickness\": 2, # 2m thickness\n", + " \"increment\": 20, # contour line every 20 meters\n", + " \"thickness\": 2, # 2m thickness\n", " \"minz\": 1600,\n", - " \"maxz\": 2000\n", + " \"maxz\": 2000,\n", " }\n", " ),\n", - " }\n", + " },\n", ").json()\n", "\n", "bounds = r[\"bounds\"]\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=r[\"minzoom\"]\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=r[\"minzoom\"],\n", ")\n", "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"Yo!!\"\n", - ")\n", - "aod_layer.add_to(m)\n", + "TileLayer(tiles=r[\"tiles\"][0], opacity=1, attr=\"Yo!!\").add_to(m)\n", "m" ] }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "id": "1c80efe0", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 35, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": url,\n", " \"algorithm\": \"contours\",\n", " \"algorithm_params\": json.dumps(\n", " {\n", - " \"increment\": 5, # contour line every 5 meters\n", - " \"thickness\": 1, # 1m thickness\n", + " \"increment\": 5, # contour line every 5 meters\n", + " \"thickness\": 1, # 1m thickness\n", " \"minz\": 1600,\n", - " \"maxz\": 2000\n", + " \"maxz\": 2000,\n", " }\n", " ),\n", - " }\n", + " },\n", ").json()\n", "\n", "bounds = r[\"bounds\"]\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=r[\"minzoom\"]\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=r[\"minzoom\"],\n", ")\n", "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"Yo!!\"\n", - ")\n", - "aod_layer.add_to(m)\n", + "TileLayer(tiles=r[\"tiles\"][0], opacity=1, attr=\"Yo!!\").add_to(m)\n", "m" ] }, @@ -755,9 +303,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 with Fil", + "display_name": "py39", "language": "python", - "name": "filprofile" + "name": "python3" }, "language_info": { "codemirror_mode": { @@ -769,12 +317,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13 (main, May 24 2022, 21:13:51) \n[Clang 13.1.6 (clang-1316.0.21.2)]" - }, - "vscode": { - "interpreter": { - "hash": "8a5edab282632443219e051e4ade2d1d5bbc671c781051bf1437897cbdfea0f1" - } + "version": "3.9.18" } }, "nbformat": 4, diff --git a/docs/src/examples/notebooks/Working_with_CloudOptimizedGeoTIFF.ipynb b/docs/src/examples/notebooks/Working_with_CloudOptimizedGeoTIFF.ipynb index 94b3be3bc..dcc679ad8 100644 --- a/docs/src/examples/notebooks/Working_with_CloudOptimizedGeoTIFF.ipynb +++ b/docs/src/examples/notebooks/Working_with_CloudOptimizedGeoTIFF.ipynb @@ -42,6 +42,7 @@ "outputs": [], "source": [ "import os\n", + "import datetime\n", "import json\n", "import urllib.parse\n", "from io import BytesIO\n", @@ -49,7 +50,7 @@ "from concurrent import futures\n", "\n", "import httpx\n", - "\n", + "import numpy\n", "from boto3.session import Session as boto3_session\n", "\n", "from rasterio.plot import reshape_as_image\n", @@ -57,9 +58,10 @@ "\n", "from tqdm.notebook import tqdm\n", "\n", - "from folium import Map, TileLayer\n", + "from folium import Map, TileLayer, GeoJson\n", "\n", - "%pylab inline" + "import matplotlib.pyplot as plt\n", + "import matplotlib.dates as mdates" ] }, { @@ -68,7 +70,9 @@ "metadata": {}, "outputs": [], "source": [ - "titiler_endpoint = \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind." + "titiler_endpoint = (\n", + " \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + ")" ] }, { @@ -86,40 +90,25 @@ "source": [ "# use geojson.io\n", "geojson = {\n", - " \"type\": \"FeatureCollection\",\n", - " \"features\": [\n", - " {\n", - " \"type\": \"Feature\",\n", - " \"properties\": {},\n", - " \"geometry\": {\n", - " \"type\": \"Polygon\",\n", - " \"coordinates\": [\n", - " [\n", - " [\n", - " -74.1796875,\n", - " 45.18978009667531\n", - " ],\n", - " [\n", - " -73.092041015625,\n", - " 45.18978009667531\n", - " ],\n", - " [\n", - " -73.092041015625,\n", - " 46.00459325574482\n", - " ],\n", - " [\n", - " -74.1796875,\n", - " 46.00459325574482\n", - " ],\n", - " [\n", - " -74.1796875,\n", - " 45.18978009667531\n", - " ]\n", - " ]\n", - " ]\n", - " }\n", - " }\n", - " ]\n", + " \"type\": \"FeatureCollection\",\n", + " \"features\": [\n", + " {\n", + " \"type\": \"Feature\",\n", + " \"properties\": {},\n", + " \"geometry\": {\n", + " \"type\": \"Polygon\",\n", + " \"coordinates\": [\n", + " [\n", + " [-74.1796875, 45.18978009667531],\n", + " [-73.092041015625, 45.18978009667531],\n", + " [-73.092041015625, 46.00459325574482],\n", + " [-74.1796875, 46.00459325574482],\n", + " [-74.1796875, 45.18978009667531],\n", + " ]\n", + " ],\n", + " },\n", + " }\n", + " ],\n", "}\n", "\n", "bounds = featureBounds(geojson)" @@ -131,11 +120,14 @@ "metadata": {}, "outputs": [], "source": [ - "Map(\n", + "m = Map(\n", " tiles=\"OpenStreetMap\",\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=6\n", - ")" + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=6,\n", + ")\n", + "\n", + "GeoJson(geojson).add_to(m)\n", + "m" ] }, { @@ -169,22 +161,29 @@ "session = boto3_session(region_name=\"us-west-2\")\n", "client = session.client(\"s3\")\n", "\n", - "bucket = \"omi-no2-nasa\" #https://registry.opendata.aws/omi-no2-nasa/\n", + "bucket = \"omi-no2-nasa\" # https://registry.opendata.aws/omi-no2-nasa/\n", "\n", "\n", "def list_objects(bucket, prefix):\n", " \"\"\"AWS s3 list objects.\"\"\"\n", "\n", - " paginator = client.get_paginator('list_objects_v2')\n", + " paginator = client.get_paginator(\"list_objects_v2\")\n", "\n", " files = []\n", " for subset in paginator.paginate(Bucket=bucket, Prefix=prefix):\n", " files.extend(subset.get(\"Contents\", []))\n", "\n", - " return [r[\"Key\"] for r in files]\n", + " return files\n", + "\n", + "\n", + "list_files = list_objects(bucket, \"OMI-Aura_L3\")\n", + "\n", + "print(\"Archive Size\")\n", + "files = [r[\"Key\"] for r in list_files]\n", + "print(f\"Found {len(files)} OMI-NO2 files\")\n", "\n", - "files = list_objects(bucket, \"OMI-Aura_L3\")\n", - "print(f\"Found : {len(files)}\")" + "size = sum([r[\"Size\"] / 1000000.0 for r in list_files])\n", + "print(f\"Size of the archive: {size} Mo ({size / 1000} Go)\")" ] }, { @@ -227,7 +226,11 @@ }, "outputs": [], "source": [ - "files_Oct5 = list(filter(lambda x: (x.split(\"_\")[2][5:7] == \"10\") & (x.split(\"_\")[2][7:9] == \"05\"), files))\n", + "files_Oct5 = list(\n", + " filter(\n", + " lambda x: (x.split(\"_\")[2][5:7] == \"10\") & (x.split(\"_\")[2][7:9] == \"05\"), files\n", + " )\n", + ")\n", "print(len(files_Oct5))\n", "print(files_Oct5)" ] @@ -238,10 +241,10 @@ "source": [ "### DATA Endpoint\n", "\n", - "`{endpoint}/cog/tiles/{z}/{x}/{y}.{format}?url={cog}&{otherquery params}`\n", + "`{endpoint}/cog/tiles/{tileMatrixSetId}/{z}/{x}/{y}.{format}?url={cog}&{otherquery params}`\n", "\n", "\n", - "`{endpoint}/cog/crop/{minx},{miny},{maxx},{maxy}.{format}?url={cog}&{otherquery params}`\n", + "`{endpoint}/cog/bbox/{minx},{miny},{maxx},{maxy}.{format}?url={cog}&{otherquery params}`\n", "\n", "\n", "`{endpoint}/cog/point/{minx},{miny}?url={cog}&{otherquery params}`\n" @@ -275,10 +278,7 @@ "# Fetch File Metadata to get min/max rescaling values (because the file is stored as float32)\n", "\n", "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/statistics\",\n", - " params = {\n", - " \"url\": _url(files[0])\n", - " }\n", + " f\"{titiler_endpoint}/cog/statistics\", params={\"url\": _url(files[0])}\n", ").json()\n", "\n", "print(json.dumps(r, indent=4))" @@ -291,25 +291,24 @@ "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": _url(files[2]),\n", " \"rescale\": \"0,3000000000000000\",\n", " \"colormap_name\": \"viridis\",\n", - " }\n", + " },\n", ").json()\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=6\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=6\n", ")\n", "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"NASA\"\n", + "TileLayer(tiles=r[\"tiles\"][0], opacity=1, attr=\"NASA\").add_to(m)\n", + "\n", + "GeoJson(geojson, style_function=lambda feature: {\"fill\": False, \"color\": \"red\"}).add_to(\n", + " m\n", ")\n", - "aod_layer.add_to(m)\n", + "\n", "m" ] }, @@ -334,8 +333,9 @@ "\n", "xmin, ymin, xmax, ymax = bounds\n", "\n", + "\n", "def fetch_bbox(file):\n", - " url = f\"{titiler_endpoint}/cog/crop/{xmin},{ymin},{xmax},{ymax}.npy\"\n", + " url = f\"{titiler_endpoint}/cog/bbox/{xmin},{ymin},{xmax},{ymax}.npy\"\n", " params = {\n", " \"url\": _url(file),\n", " \"bidx\": \"1\",\n", @@ -344,7 +344,10 @@ " r = httpx.get(url, params=params)\n", " data = numpy.load(BytesIO(r.content))\n", " s = _stats(data[0:-1], data[-1])\n", - " return s[1], file.split(\"_\")[2]\n", + " return (\n", + " _stats(data[0:-1], data[-1]),\n", + " datetime.datetime.strptime(file.split(\"_\")[2].replace(\"m\", \"\"), \"%Y%m%d\"),\n", + " )\n", "\n", "\n", "# small tool to filter invalid response from the API\n", @@ -382,19 +385,20 @@ "outputs": [], "source": [ "with futures.ThreadPoolExecutor(max_workers=10) as executor:\n", - " future_work = [\n", - " executor.submit(fetch_bbox, file) for file in files_15\n", - " ]\n", + " future_work = [executor.submit(fetch_bbox, file) for file in files_15]\n", "\n", - " for f in tqdm(futures.as_completed(future_work), total=len(future_work)): \n", + " for f in tqdm(futures.as_completed(future_work), total=len(future_work)):\n", " pass\n", "\n", - "values, dates = zip(*list(_filter_futures(future_work)))\n", + "values, dates = zip(*list(_filter_futures(future_work)))\n", "\n", - "fig, ax1 = plt.subplots(dpi=150)\n", + "max_values = [v[1] for v in values]\n", + "\n", + "fig, ax1 = plt.subplots(dpi=300)\n", "fig.autofmt_xdate()\n", "\n", - "ax1.plot(dates, values, label=\"No2\")\n", + "ax1.plot(dates, max_values, label=\"No2\")\n", + "ax1.xaxis.set_major_locator(mdates.YearLocator(1, 7))\n", "\n", "ax1.set_xlabel(\"Dates\")\n", "ax1.set_ylabel(\"No2\")\n", @@ -416,19 +420,20 @@ "outputs": [], "source": [ "with futures.ThreadPoolExecutor(max_workers=50) as executor:\n", - " future_work = [\n", - " executor.submit(fetch_bbox, file) for file in files\n", - " ]\n", + " future_work = [executor.submit(fetch_bbox, file) for file in files]\n", "\n", - " for f in tqdm(futures.as_completed(future_work), total=len(future_work)): \n", + " for f in tqdm(futures.as_completed(future_work), total=len(future_work)):\n", " pass\n", "\n", - "values, dates = zip(*list(_filter_futures(future_work)))\n", + "values, dates = zip(*list(_filter_futures(future_work)))\n", + "\n", + "max_values = [v[1] for v in values]\n", "\n", "fig, ax1 = plt.subplots(dpi=150)\n", "fig.autofmt_xdate()\n", "\n", - "ax1.plot(dates, values, label=\"No2\")\n", + "ax1.plot(dates, max_values, label=\"No2\")\n", + "ax1.xaxis.set_major_locator(mdates.YearLocator())\n", "\n", "ax1.set_xlabel(\"Dates\")\n", "ax1.set_ylabel(\"No2\")\n", @@ -460,7 +465,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.2" + "version": "3.9.18" } }, "nbformat": 4, diff --git a/docs/src/examples/notebooks/Working_with_CloudOptimizedGeoTIFF_simple.ipynb b/docs/src/examples/notebooks/Working_with_CloudOptimizedGeoTIFF_simple.ipynb index 78c4509ea..68fc67891 100644 --- a/docs/src/examples/notebooks/Working_with_CloudOptimizedGeoTIFF_simple.ipynb +++ b/docs/src/examples/notebooks/Working_with_CloudOptimizedGeoTIFF_simple.ipynb @@ -50,7 +50,9 @@ "metadata": {}, "outputs": [], "source": [ - "titiler_endpoint = \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + "titiler_endpoint = (\n", + " \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + ")\n", "url = \"https://opendata.digitalglobe.com/events/mauritius-oil-spill/post-event/2020-08-12/105001001F1B5B00/105001001F1B5B00.tif\"" ] }, @@ -70,9 +72,9 @@ "# Fetch File Metadata to get min/max rescaling values (because the file is stored as float32)\n", "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/info\",\n", - " params = {\n", + " params={\n", " \"url\": url,\n", - " }\n", + " },\n", ").json()\n", "\n", "bounds = r[\"bounds\"]\n", @@ -95,9 +97,9 @@ "# Fetch File Metadata to get min/max rescaling values (because the file is stored as float32)\n", "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/statistics\",\n", - " params = {\n", + " params={\n", " \"url\": url,\n", - " }\n", + " },\n", ").json()\n", "\n", "print(json.dumps(r, indent=4))" @@ -117,23 +119,18 @@ "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": url,\n", - " }\n", + " },\n", ").json()\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=13\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=13\n", ")\n", "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"DigitalGlobe OpenData\"\n", - ")\n", - "aod_layer.add_to(m)\n", + "TileLayer(tiles=r[\"tiles\"][0], opacity=1, attr=\"DigitalGlobe OpenData\").add_to(m)\n", + "\n", "m" ] }, @@ -150,24 +147,25 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "url = \"https://data.geo.admin.ch/ch.swisstopo.swissalti3d/swissalti3d_2019_2573-1085/swissalti3d_2019_2573-1085_0.5_2056_5728.tif\"\n", "\n", "# Fetch File Metadata to get min/max rescaling values (because the file is stored as float32)\n", "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/info\",\n", - " params = {\n", + " params={\n", " \"url\": url,\n", - " }\n", + " },\n", ").json()\n", "\n", "print(r)\n", "print(\"Data is of type:\", r[\"dtype\"])\n", "\n", "# This dataset has statistics metadata\n", - "minv, maxv = r[\"band_metadata\"][0][1][\"STATISTICS_MINIMUM\"], r[\"band_metadata\"][0][1][\"STATISTICS_MAXIMUM\"]\n", - "print(\"With values from \", minv, \"to \", maxv)\n", - "\n" + "minv, maxv = (\n", + " r[\"band_metadata\"][0][1][\"STATISTICS_MINIMUM\"],\n", + " r[\"band_metadata\"][0][1][\"STATISTICS_MAXIMUM\"],\n", + ")\n", + "print(\"With values from \", minv, \"to \", maxv)" ] }, { @@ -179,12 +177,12 @@ "# We could get the min/max values using the statistics endpoint\n", "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/statistics\",\n", - " params = {\n", + " params={\n", " \"url\": url,\n", - " }\n", + " },\n", ").json()\n", "\n", - "print(json.dumps(r[\"1\"], indent=4))" + "print(json.dumps(r[\"b1\"], indent=4))" ] }, { @@ -194,7 +192,7 @@ "### Display Tiles\n", "\n", "\n", - "1. Without `rescaling` values, TiTiler will return black/grey tiles because it will rescale the data base on min/max value of the datatype." + "Note: By default if the metadata has `min/max` statistics, titiler will use those to rescale the data" ] }, { @@ -204,24 +202,19 @@ "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": url,\n", - " }\n", + " },\n", ").json()\n", "\n", "bounds = r[\"bounds\"]\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=r[\"minzoom\"] + 1\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=r[\"minzoom\"] + 1,\n", ")\n", "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"Swisstopo\"\n", - ")\n", - "aod_layer.add_to(m)\n", + "TileLayer(tiles=r[\"tiles\"][0], opacity=1, attr=\"Swisstopo\").add_to(m)\n", "m" ] }, @@ -229,45 +222,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "2. Apply linear rescaling using Min/Max value \n", - "\n", - "This is needed to rescale the value to byte (0 -> 255) which can then be encoded in JPEG or PNG" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", - " \"url\": url,\n", - " \"rescale\": f\"{minv},{maxv}\"\n", - " }\n", - ").json()\n", - "\n", - "bounds = r[\"bounds\"]\n", - "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=r[\"minzoom\"] + 1\n", - ")\n", - "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"Swisstopo\"\n", - ")\n", - "aod_layer.add_to(m)\n", - "m" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "3. Apply ColorMap\n", + "Apply ColorMap\n", "\n", "Now that the data is rescaled to byte values (0 -> 255) we can apply a colormap" ] @@ -279,26 +234,17 @@ "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", - " \"url\": url,\n", - " \"rescale\": f\"{minv},{maxv}\",\n", - " \"colormap_name\": \"terrain\"\n", - " }\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\"url\": url, \"rescale\": f\"{minv},{maxv}\", \"colormap_name\": \"terrain\"},\n", ").json()\n", "\n", "bounds = r[\"bounds\"]\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=r[\"minzoom\"] + 1\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=r[\"minzoom\"] + 1,\n", ")\n", "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"Swisstopo\"\n", - ")\n", - "aod_layer.add_to(m)\n", + "TileLayer(tiles=r[\"tiles\"][0], opacity=1, attr=\"Swisstopo\").add_to(m)\n", "m" ] }, @@ -306,7 +252,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "4. Apply non-linear colormap (intervals)\n", + "Apply non-linear colormap (intervals)\n", "\n", "see https://cogeotiff.github.io/rio-tiler/colormap/#intervals-colormaps" ] @@ -322,34 +268,27 @@ "cmap = json.dumps(\n", " [\n", " # ([min, max], [r, g, b, a])\n", - " ([0, 1500], [255,255,204, 255]),\n", - " ([1500, 1700], [161,218,180, 255]),\n", - " ([1700, 1900], [65,182,196, 255]),\n", - " ([1900, 2000], [44,127,184, 255]),\n", - " ([2000, 3000], [37,52,148, 255]),\n", + " ([0, 1500], [255, 255, 204, 255]),\n", + " ([1500, 1700], [161, 218, 180, 255]),\n", + " ([1700, 1900], [65, 182, 196, 255]),\n", + " ([1900, 2000], [44, 127, 184, 255]),\n", + " ([2000, 3000], [37, 52, 148, 255]),\n", " ]\n", ")\n", "# https://colorbrewer2.org/#type=sequential&scheme=YlGnBu&n=5\n", "\n", "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\",\n", - " params = {\n", - " \"url\": url,\n", - " \"colormap\": cmap\n", - " }\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\",\n", + " params={\"url\": url, \"colormap\": cmap},\n", ").json()\n", "\n", "bounds = r[\"bounds\"]\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=r[\"minzoom\"] + 1\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=r[\"minzoom\"] + 1,\n", ")\n", "\n", - "aod_layer = TileLayer(\n", - " tiles=r[\"tiles\"][0],\n", - " opacity=1,\n", - " attr=\"Swisstopo\"\n", - ")\n", + "aod_layer = TileLayer(tiles=r[\"tiles\"][0], opacity=1, attr=\"Swisstopo\")\n", "aod_layer.add_to(m)\n", "m" ] @@ -378,7 +317,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.2" + "version": "3.9.18" } }, "nbformat": 4, diff --git a/docs/src/examples/notebooks/Working_with_MosaicJSON.ipynb b/docs/src/examples/notebooks/Working_with_MosaicJSON.ipynb index b62c9220e..40a54b4f7 100644 --- a/docs/src/examples/notebooks/Working_with_MosaicJSON.ipynb +++ b/docs/src/examples/notebooks/Working_with_MosaicJSON.ipynb @@ -1,7 +1,6 @@ { "cells": [ { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -30,7 +29,7 @@ "\n", "By default, TiTiler has `mosaicjson` endpoints.\n", "\n", - "Docs: https://api.cogeo.xyz/docs#/MosaicJSON" + "Docs: https://titiler.xyz/api.html#/MosaicJSON" ] }, { @@ -118,13 +117,13 @@ "session = boto3_session(region_name=\"us-west-2\")\n", "client = session.client(\"s3\")\n", "\n", - "bucket = \"noaa-eri-pds\" #https://registry.opendata.aws/omi-no2-nasa/\n", + "bucket = \"noaa-eri-pds\" # https://registry.opendata.aws/omi-no2-nasa/\n", "\n", "\n", "def list_objects(bucket, prefix):\n", " \"\"\"AWS s3 list objects.\"\"\"\n", "\n", - " paginator = client.get_paginator('list_objects_v2')\n", + " paginator = client.get_paginator(\"list_objects_v2\")\n", "\n", " files = []\n", " for subset in paginator.paginate(Bucket=bucket, Prefix=prefix):\n", @@ -132,6 +131,7 @@ "\n", " return [r[\"Key\"] for r in files]\n", "\n", + "\n", "files = list_objects(bucket, \"2020_Nashville_Tornado/20200307a_RGB\")\n", "files = [f\"s3://{bucket}/{f}\" for f in files if f.endswith(\".tif\")]\n", "\n", @@ -163,8 +163,8 @@ "outputs": [], "source": [ "# We can derive the `bbox` from the filename\n", - "# s3://noaa-eri-pds/2020_Nashville_Tornado/20200307a_RGB/20200307aC0870130w361200n.tif \n", - "# -> 20200307aC0870130w361200n.tif \n", + "# s3://noaa-eri-pds/2020_Nashville_Tornado/20200307a_RGB/20200307aC0870130w361200n.tif\n", + "# -> 20200307aC0870130w361200n.tif\n", "# -> 20200307aC \"0870130w\" \"361200n\" .tif\n", "# -> 0870130w -> 87.025 (West)\n", "# -> 361200n -> 36.2 (Top)\n", @@ -174,16 +174,20 @@ "from geojson_pydantic.features import Feature\n", "from geojson_pydantic.geometries import Polygon\n", "\n", + "\n", "def dms_to_degree(v: str) -> float:\n", " \"\"\"convert degree minute second to decimal degrees.\n", - " \n", + "\n", " '0870130w' -> 87.025\n", " \"\"\"\n", " deg = int(v[0:3])\n", " minutes = int(v[3:5])\n", " seconds = int(v[5:7])\n", " direction = v[-1].upper()\n", - " return (float(deg) + float(minutes)/60 + float(seconds)/(60*60)) * (-1 if direction in ['W', 'S'] else 1)\n", + " return (float(deg) + float(minutes) / 60 + float(seconds) / (60 * 60)) * (\n", + " -1 if direction in [\"W\", \"S\"] else 1\n", + " )\n", + "\n", "\n", "def fname_to_feature(src_path: str) -> Feature:\n", " bname = os.path.basename(src_path)\n", @@ -194,16 +198,14 @@ " lat = dms_to_degree(\"0\" + lat_dms)\n", "\n", " return Feature(\n", - " geometry=Polygon.from_bounds(\n", - " lon, lat - 0.025, lon + 0.025, lat \n", - " ),\n", + " geometry=Polygon.from_bounds(lon, lat - 0.025, lon + 0.025, lat),\n", " properties={\n", " \"path\": src_path,\n", - " }\n", + " },\n", " )\n", - "features = [\n", - " fname_to_feature(f).dict(exclude_none=True) for f in files\n", - "]\n", + "\n", + "\n", + "features = [fname_to_feature(f).dict(exclude_none=True) for f in files]\n", "\n", "# OR We could use Rasterio/rio-tiler\n", "\n", @@ -231,20 +233,23 @@ "metadata": {}, "outputs": [], "source": [ - "geojson = {'type': 'FeatureCollection', 'features': features}\n", + "geojson = {\"type\": \"FeatureCollection\", \"features\": features}\n", "\n", "bounds = featureBounds(geojson)\n", "\n", "m = Map(\n", " tiles=\"OpenStreetMap\",\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=6\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=6,\n", ")\n", "\n", "geo_json = GeoJson(\n", " data=geojson,\n", " style_function=lambda x: {\n", - " 'opacity': 1, 'dashArray': '1', 'fillOpacity': 0, 'weight': 1\n", + " \"opacity\": 1,\n", + " \"dashArray\": \"1\",\n", + " \"fillOpacity\": 0,\n", + " \"weight\": 1,\n", " },\n", ")\n", "geo_json.add_to(m)\n", @@ -280,10 +285,11 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "# We are creating the mosaicJSON using the features we created earlier\n", "# by default MosaicJSON.from_feature will look in feature.properties.path to get the path of the dataset\n", - "mosaicdata = MosaicJSON.from_features(features, minzoom=info.minzoom, maxzoom=info.maxzoom)\n", + "mosaicdata = MosaicJSON.from_features(\n", + " features, minzoom=info.minzoom, maxzoom=info.maxzoom\n", + ")\n", "with MosaicBackend(\"NOAA_Nashville_Tornado.json.gz\", mosaic_def=mosaicdata) as mosaic:\n", " mosaic.write(overwrite=True)\n", " print(mosaic.info())" @@ -297,20 +303,21 @@ }, "outputs": [], "source": [ - "titiler_endpoint = \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + "titiler_endpoint = (\n", + " \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + ")\n", "\n", "r = httpx.get(\n", - " f\"{titiler_endpoint}/mosaicjson/tilejson.json\",\n", + " f\"{titiler_endpoint}/mosaicjson/WebMercatorQuad/tilejson.json\",\n", " params={\n", " # For this demo we are use the same mosaic but stored on the web\n", - " \"url\": \"https://gist.githubusercontent.com/vincentsarago/c6ace3ccd29a82a4a5531693bbcd61fc/raw/e0d0174a64a9acd2fb820f2c65b1830aab80f52b/NOAA_Nashville_Tornado.json\" \n", - " }\n", + " \"url\": \"https://gist.githubusercontent.com/vincentsarago/c6ace3ccd29a82a4a5531693bbcd61fc/raw/e0d0174a64a9acd2fb820f2c65b1830aab80f52b/NOAA_Nashville_Tornado.json\"\n", + " },\n", ").json()\n", "print(r)\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=13\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=13\n", ")\n", "\n", "tiles = TileLayer(\n", @@ -318,13 +325,16 @@ " min_zoom=r[\"minzoom\"],\n", " max_zoom=r[\"maxzoom\"],\n", " opacity=1,\n", - " attr=\"NOAA\"\n", + " attr=\"NOAA\",\n", ")\n", "\n", "geo_json = GeoJson(\n", " data=geojson,\n", " style_function=lambda x: {\n", - " 'opacity': 1, 'dashArray': '1', 'fillOpacity': 0, 'weight': 1\n", + " \"opacity\": 1,\n", + " \"dashArray\": \"1\",\n", + " \"fillOpacity\": 0,\n", + " \"weight\": 1,\n", " },\n", ")\n", "tiles.add_to(m)\n", @@ -342,7 +352,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -356,7 +366,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.2" + "version": "3.9.17" } }, "nbformat": 4, diff --git a/docs/src/examples/notebooks/Working_with_NumpyTile.ipynb b/docs/src/examples/notebooks/Working_with_NumpyTile.ipynb index b6b580506..d3e46e00e 100755 --- a/docs/src/examples/notebooks/Working_with_NumpyTile.ipynb +++ b/docs/src/examples/notebooks/Working_with_NumpyTile.ipynb @@ -54,7 +54,9 @@ "metadata": {}, "outputs": [], "source": [ - "titiler_endpoint = \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + "titiler_endpoint = (\n", + " \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + ")\n", "url = \"https://opendata.digitalglobe.com/events/mauritius-oil-spill/post-event/2020-08-12/105001001F1B5B00/105001001F1B5B00.tif\"" ] }, @@ -64,7 +66,7 @@ "metadata": {}, "outputs": [], "source": [ - "r = httpx.get(f\"{titiler_endpoint}/cog/tilejson.json?url={url}\").json()\n", + "r = httpx.get(f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json?url={url}\").json()\n", "print(r)" ] }, @@ -88,7 +90,9 @@ "# Call TiTiler endpoint using the first tile\n", "\n", "tile = tiles[0]\n", - "r = httpx.get(f\"{titiler_endpoint}/cog/tiles/{tile.z}/{tile.x}/{tile.y}.npy?url={url}\")" + "r = httpx.get(\n", + " f\"{titiler_endpoint}/cog/tiles/WebMercatorQuad/{tile.z}/{tile.x}/{tile.y}.npy?url={url}\"\n", + ")" ] }, { diff --git a/docs/src/examples/notebooks/Working_with_STAC.ipynb b/docs/src/examples/notebooks/Working_with_STAC.ipynb index 4d4660d52..24149b85f 100644 --- a/docs/src/examples/notebooks/Working_with_STAC.ipynb +++ b/docs/src/examples/notebooks/Working_with_STAC.ipynb @@ -1,7 +1,6 @@ { "cells": [ { - "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ @@ -30,7 +29,7 @@ "\n", "# TiTiler: STAC + COG\n", "\n", - "Docs: https://github.com/developmentseed/titiler/blob/main/docs/endpoints/stac.md\n", + "Docs: https://github.com/developmentseed/titiler/blob/main/docs/src/endpoints/stac.md\n", "\n", "\n", "TiTiler was first designed to work with single COG by passing the file URL to the tiler.\n", @@ -106,7 +105,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -116,20 +115,11 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "%pylab is deprecated, use %matplotlib inline and import the required libraries.\n", - "Populating the interactive namespace from numpy and matplotlib\n" - ] - } - ], + "outputs": [], "source": [ "import os\n", "import json\n", @@ -155,12 +145,14 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Endpoint variables\n", - "titiler_endpoint = \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + "titiler_endpoint = (\n", + " \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + ")\n", "stac_endpoint = \"https://earth-search.aws.element84.com/v0/search\"" ] }, @@ -179,148 +171,38 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "geojson = {\n", - " \"type\": \"FeatureCollection\",\n", - " \"features\": [\n", - " {\n", - " \"type\": \"Feature\",\n", - " \"properties\": {},\n", - " \"geometry\": {\n", - " \"type\": \"Polygon\",\n", - " \"coordinates\": [\n", - " [\n", - " [\n", - " 30.810813903808594,\n", - " 29.454247067148533\n", - " ],\n", - " [\n", - " 30.88600158691406,\n", - " 29.454247067148533\n", - " ],\n", - " [\n", - " 30.88600158691406,\n", - " 29.51879923863822\n", - " ],\n", - " [\n", - " 30.810813903808594,\n", - " 29.51879923863822\n", - " ],\n", - " [\n", - " 30.810813903808594,\n", - " 29.454247067148533\n", - " ]\n", - " ]\n", - " ]\n", - " }\n", - " }\n", - " ]\n", + " \"type\": \"FeatureCollection\",\n", + " \"features\": [\n", + " {\n", + " \"type\": \"Feature\",\n", + " \"properties\": {},\n", + " \"geometry\": {\n", + " \"type\": \"Polygon\",\n", + " \"coordinates\": [\n", + " [\n", + " [30.810813903808594, 29.454247067148533],\n", + " [30.88600158691406, 29.454247067148533],\n", + " [30.88600158691406, 29.51879923863822],\n", + " [30.810813903808594, 29.51879923863822],\n", + " [30.810813903808594, 29.454247067148533],\n", + " ]\n", + " ],\n", + " },\n", + " }\n", + " ],\n", "}\n", "\n", "bounds = featureBounds(geojson)\n", "\n", "m = Map(\n", " tiles=\"OpenStreetMap\",\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=11\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=11,\n", ")\n", "\n", "geo_json = GeoJson(data=geojson)\n", @@ -337,83 +219,36 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Results context:\n", - "{'page': 1, 'limit': 100, 'matched': 85, 'returned': 85}\n", - "\n", - "Example of item:\n", - "{\n", - " \"bbox\": [\n", - " 30.155974613579858,\n", - " 28.80949327971016,\n", - " 31.050481437029678,\n", - " 29.815791988006527\n", - " ],\n", - " \"geometry\": {\n", - " \"coordinates\": [\n", - " [\n", - " [\n", - " 30.155974613579858,\n", - " 28.80949327971016\n", - " ],\n", - " [\n", - " 30.407037927198104,\n", - " 29.805008695373978\n", - " ],\n", - " [\n", - " 31.031551610920825,\n", - " 29.815791988006527\n", - " ],\n", - " [\n", - " 31.050481437029678,\n", - " 28.825387639743422\n", - " ],\n", - " [\n", - " 30.155974613579858,\n", - " 28.80949327971016\n", - " ]\n", - " ]\n", - " ],\n", - " \"type\": \"Polygon\"\n", - " },\n", - " \"id\": \"S2B_36RTT_20191205_0_L2A\",\n", - " \"collection\": \"sentinel-s2-l2a-cogs\",\n", - " \"type\": \"Feature\",\n", - " \"properties\": {\n", - " \"datetime\": \"2019-12-05T08:42:04Z\",\n", - " \"eo:cloud_cover\": 2.75\n", - " }\n", - "}\n" - ] - } - ], + "outputs": [], "source": [ - "start = datetime.datetime.strptime(\"2019-01-01\", \"%Y-%m-%d\").strftime(\"%Y-%m-%dT00:00:00Z\")\n", - "end = datetime.datetime.strptime(\"2019-12-11\", \"%Y-%m-%d\").strftime(\"%Y-%m-%dT23:59:59Z\")\n", + "start = datetime.datetime.strptime(\"2019-01-01\", \"%Y-%m-%d\").strftime(\n", + " \"%Y-%m-%dT00:00:00Z\"\n", + ")\n", + "end = datetime.datetime.strptime(\"2019-12-11\", \"%Y-%m-%d\").strftime(\n", + " \"%Y-%m-%dT23:59:59Z\"\n", + ")\n", "\n", "# POST body\n", "query = {\n", " \"collections\": [\"sentinel-s2-l2a-cogs\"],\n", " \"datetime\": f\"{start}/{end}\",\n", " \"query\": {\n", - " \"eo:cloud_cover\": {\n", - " \"lt\": 5\n", - " },\n", + " \"eo:cloud_cover\": {\"lt\": 5},\n", " },\n", " \"intersects\": geojson[\"features\"][0][\"geometry\"],\n", " \"limit\": 100,\n", " \"fields\": {\n", - " 'include': ['id', 'properties.datetime', 'properties.eo:cloud_cover'], # This will limit the size of returned body\n", - " 'exclude': ['assets', 'links'] # This will limit the size of returned body\n", - " }\n", + " \"include\": [\n", + " \"id\",\n", + " \"properties.datetime\",\n", + " \"properties.eo:cloud_cover\",\n", + " ], # This will limit the size of returned body\n", + " \"exclude\": [\"assets\", \"links\"], # This will limit the size of returned body\n", + " },\n", "}\n", "\n", "# POST Headers\n", @@ -437,122 +272,23 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "m = Map(\n", " tiles=\"OpenStreetMap\",\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=8\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=8,\n", ")\n", "\n", "geo_json = GeoJson(\n", " data=data,\n", " style_function=lambda x: {\n", - " 'opacity': 1, 'dashArray': '1', 'fillOpacity': 0, 'weight': 1\n", + " \"opacity\": 1,\n", + " \"dashArray\": \"1\",\n", + " \"fillOpacity\": 0,\n", + " \"weight\": 1,\n", " },\n", ")\n", "geo_json.add_to(m)\n", @@ -568,35 +304,21 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAigAAAFyCAYAAADf8CGJAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/NK7nSAAAACXBIWXMAAA9hAAAPYQGoP6dpAAC1M0lEQVR4nOz9d5gc5ZnvjX8rd+6ePBpplFBECRACZHIGkTE2YExyNvLu+njPb/dwzr676/PuLnvevd59ffYsOAM2wRhMNBgwUWQQQaCcszQaTeqezl1Vz++P1vR0qNzVYUbP57p0XdNV1U89mumuuut+7vv7ZQghBBQKhUKhUChNBNvoCVAoFAqFQqGUQwMUCoVCoVAoTQcNUCgUCoVCoTQdNEChUCgUCoXSdNAAhUKhUCgUStNBAxQKhUKhUChNBw1QKBQKhUKhNB00QKFQKBQKhdJ08PU+oaqqOHToEILBIBiGqffpKRQKhUKhOIAQgtHRUfT09IBla5/fqHuAcujQIfT29tb7tBQKhUKhUFxg//79mDZtWs3PU/cAJRgMAsj/B0OhUL1PT6FQKBQKxQGxWAy9vb2F+3itqXuAMrasEwqFaIBCoVAoFMoEo17lGbRIlkKhUCgUStNBAxQKhUKhUChNh60A5R//8R/BMEzJvwULFtRqbhQKhUKhUI5TbNegLFq0CK+++ur4AHzdy1goFAqF4hKKoiCXyzV6GpQmQBAEcBzX6GkUsB1d8DyP7u7uWsyFQqFQKHWCEIK+vj6MjIw0eiqUJiISiaC7u7spdMpsByjbt29HT08PPB4PVq5ciXvuuQfTp0/XPT6TySCTyRRex2IxZzOlUCgUimuMBSednZ3w+XxNcUOiNA5CCJLJJPr7+wEAU6ZMafCMbAYop59+Oh588EHMnz8fhw8fxo9//GOcffbZ2LBhg25f9D333IMf//jHrkyWQqFQKNWjKEohOGlra2v0dChNgtfrBQD09/ejs7Oz4cs9DCGEOH3zyMgIZsyYgX//93/HN7/5Tc1jtDIovb29iEajVAeFQqFQGkA6ncbu3bsxc+bMwk2JQgGAVCqFPXv2YNasWfB4PCX7YrEYwuFw3e7fVVW4RiIRzJs3Dzt27NA9RpIkSJJUzWkoFAqFUgPosg6lnGb6TFSlgxKPx7Fz586mWKuiUCgUCoUyebAVoPzX//pfsWbNGuzZswfvvfcerrvuOnAch5tvvrlW86NQKBQKxRYMw+CZZ56p+XlmzpyJn/zkJzU/z/GKrQDlwIEDuPnmmzF//nx89atfRVtbGz744AN0dHTUan4UiiNyR44gvmZNo6dBoVBcpq+vD3/xF3+B2bNnQ5Ik9Pb24qqrrsJrr73W6KlpEovF8D/+x//AggUL4PF40N3djYsuughPPfUUqigBPS6wVYPy2GOP1WoeFIqryAMDiL/7LgLnntvoqVAoFJfYs2cPzjzzTEQiEfzbv/0blixZglwuh5dffhmrV6/Gli1bGj3FEkZGRnDWWWchGo3in/7pn7BixQrwPI81a9bgb/7mb3DBBRcgEok0ZG7ZbBaiKDbk3FahXjwTAHlwkGYD7JLLQRkYbPQsKBSKi9x1111gGAYfffQRvvzlL2PevHlYtGgRfvSjH+GDDz7Qfd/69etxwQUXwOv1oq2tDd/5zncQj8cL+8877zz88Ic/LHnPtddeizvuuKPwur+/H1dddRW8Xi9mzZqFRx55xHS+//2//3fs2bMHH374IW6//XaceOKJmDdvHr797W9j3bp1CAQCAIDh4WHcdtttaGlpgc/nw+WXX47t27cDyGdgvF4vXnzxxZKxn376aQSDQSSTSQDA/v378dWvfhWRSAStra245pprsGfPnsLxd9xxB6699lr88z//M3p6ejB//nzT+TcaGqBMADJbt9IAxSZcewd8Z5ze6GlQKBSXGBoawksvvYTVq1fD7/dX7NfLRCQSCVx66aVoaWnB2rVr8cQTT+DVV1/FD37wA1vnv+OOO7B//3688cYb+MMf/oD77ruvIGqmhaqqeOyxx3DLLbegp6enYn8gEChYxdxxxx34+OOP8dxzz+H9998HIQSrVq1CLpdDKBTClVdeiUcffbTk/Y888giuvfZa+Hw+5HI5XHrppQgGg3j77bfx7rvvIhAI4LLLLkM2my2857XXXsPWrVvxyiuv4Pnnn7f1/28E1EhnAiBMmwae1vnYIrtzB7J79jZ6GhQKxSV27NgBQohtg9pHH30U6XQav/3tbwuBzX/+53/iqquuwv/6X/8LXV1dpmNs27YNL774Ij766COsWLECAPDrX/8aCxcu1H3PwMAAhoeHTee7fft2PPfcc3j33XfxpS99CUA++Ojt7cUzzzyDr3zlK7jllltw6623IplMwufzIRaL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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "fig = plt.figure(dpi=100)\n", "fig.autofmt_xdate()\n", "ax = fig.add_subplot(1, 1, 1)\n", - "ax.plot(dates, cloudcover, label=\"Cloud Cover\", color=\"tab:red\", linewidth=0.4, linestyle=\"-.\")\n", + "ax.plot(\n", + " dates,\n", + " cloudcover,\n", + " label=\"Cloud Cover\",\n", + " color=\"tab:red\",\n", + " linewidth=0.4,\n", + " linestyle=\"-.\",\n", + ")\n", "\n", "ax.legend()" ] @@ -607,12 +329,12 @@ "source": [ "## Use Titiler endpoint\n", "\n", - "https://api.cogeo.xyz/docs#/SpatioTemporal%20Asset%20Catalog\n", + "https://titiler.xyz/api.html#/SpatioTemporal%20Asset%20Catalog\n", "\n", - "`{endpoint}/stac/tiles/{z}/{x}/{y}.{format}?url={stac_item}&{otherquery params}`\n", + "`{endpoint}/stac/tiles/{tileMatrixSetId}/{z}/{x}/{y}.{format}?url={stac_item}&{otherquery params}`\n", "\n", "\n", - "`{endpoint}/stac/crop/{minx},{miny},{maxx},{maxy}.{format}?url={stac_item}&{otherquery params}`\n", + "`{endpoint}/stac/bbox/{minx},{miny},{maxx},{maxy}.{format}?url={stac_item}&{otherquery params}`\n", "\n", "\n", "`{endpoint}/stac/point/{minx},{miny}?url={stac_item}&{otherquery params}`\n" @@ -620,7 +342,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -636,123 +358,44 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs/items/S2B_36RTT_20190102_0_L2A\n", - "{'tilejson': '2.2.0', 'version': '1.0.0', 'scheme': 'xyz', 'tiles': ['http://127.0.0.1:8081/stac/tiles/WebMercatorQuad/{z}/{x}/{y}@1x?url=https%3A%2F%2Fearth-search.aws.element84.com%2Fv0%2Fcollections%2Fsentinel-s2-l2a-cogs%2Fitems%2FS2B_36RTT_20190102_0_L2A&assets=B04&assets=B03&assets=B02&color_formula=Gamma+RGB+3.5+Saturation+1.7+Sigmoidal+RGB+15+0.35'], 'minzoom': 8, 'maxzoom': 14, 'bounds': [29.896473859714554, 28.804454491507947, 31.006314627204915, 29.815413491817537], 'center': [30.451394243459735, 29.309933991662742, 8]}\n" - ] - }, - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Get Tile URL\n", - "item = url_template.format(id=sceneid[-1])\n", + "item = url_template.format(id=sceneid[-1])\n", "print(item)\n", "r = httpx.get(\n", - " f\"{titiler_endpoint}/stac/tilejson.json\",\n", - " params = (\n", + " f\"{titiler_endpoint}/stac/WebMercatorQuad/tilejson.json\",\n", + " params=(\n", " (\"url\", item),\n", " # Simple RGB combination (True Color)\n", - " (\"assets\", \"B04\"), # red, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", - " (\"assets\", \"B03\"), # green, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", - " (\"assets\", \"B02\"), # blue, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", - " (\"color_formula\", \"Gamma RGB 3.5 Saturation 1.7 Sigmoidal RGB 15 0.35\"), # We use a rio-color formula to make the tiles look nice\n", + " (\n", + " \"assets\",\n", + " \"B04\",\n", + " ), # red, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", + " (\n", + " \"assets\",\n", + " \"B03\",\n", + " ), # green, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", + " (\n", + " \"assets\",\n", + " \"B02\",\n", + " ), # blue, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", + " (\n", + " \"color_formula\",\n", + " \"Gamma RGB 3.5 Saturation 1.7 Sigmoidal RGB 15 0.35\",\n", + " ), # We use a rio-color formula to make the tiles look nice\n", " (\"minzoom\", 8), # By default titiler will use 0\n", - " (\"maxzoom\", 14), # By default titiler will use 24\n", - " )\n", + " (\"maxzoom\", 14), # By default titiler will use 24\n", + " ),\n", ").json()\n", "print(r)\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=10\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=10\n", ")\n", "\n", "tiles = TileLayer(\n", @@ -760,7 +403,7 @@ " min_zoom=r[\"minzoom\"],\n", " max_zoom=r[\"maxzoom\"],\n", " opacity=1,\n", - " attr=\"Digital Earth Africa\"\n", + " attr=\"Digital Earth Africa\",\n", ")\n", "tiles.add_to(m)\n", "m" @@ -768,119 +411,41 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'tilejson': '2.2.0', 'version': '1.0.0', 'scheme': 'xyz', 'tiles': ['http://127.0.0.1:8081/stac/tiles/WebMercatorQuad/{z}/{x}/{y}@1x?url=https%3A%2F%2Fearth-search.aws.element84.com%2Fv0%2Fcollections%2Fsentinel-s2-l2a-cogs%2Fitems%2FS2B_36RTT_20191205_0_L2A&assets=B08&assets=B04&assets=B03&color_formula=Gamma+RGB+3.5+Saturation+1.7+Sigmoidal+RGB+15+0.35'], 'minzoom': 8, 'maxzoom': 14, 'bounds': [30.155974613579858, 28.80949327971016, 31.050481437029678, 29.815791988006527], 'center': [30.603228025304766, 29.312642633858346, 8]}\n" - ] - }, - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/stac/tilejson.json\",\n", - " params = (\n", + " f\"{titiler_endpoint}/stac/WebMercatorQuad/tilejson.json\",\n", + " params=(\n", " (\"url\", url_template.format(id=sceneid[0])),\n", " # False Color Infrared\n", - " (\"assets\", \"B08\"), # nir, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", - " (\"assets\", \"B04\"), # red, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", - " (\"assets\", \"B03\"), # green, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", - " (\"color_formula\", \"Gamma RGB 3.5 Saturation 1.7 Sigmoidal RGB 15 0.35\"), # We use a rio-color formula to make the tiles look nice\n", + " (\n", + " \"assets\",\n", + " \"B08\",\n", + " ), # nir, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", + " (\n", + " \"assets\",\n", + " \"B04\",\n", + " ), # red, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", + " (\n", + " \"assets\",\n", + " \"B03\",\n", + " ), # green, in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", + " (\n", + " \"color_formula\",\n", + " \"Gamma RGB 3.5 Saturation 1.7 Sigmoidal RGB 15 0.35\",\n", + " ), # We use a rio-color formula to make the tiles look nice\n", " (\"minzoom\", 8), # By default titiler will use 0\n", - " (\"maxzoom\", 14), # By default titiler will use 24\n", - " )\n", + " (\"maxzoom\", 14), # By default titiler will use 24\n", + " ),\n", ").json()\n", "print(r)\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=10\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=10\n", ")\n", "\n", "tiles = TileLayer(\n", @@ -888,7 +453,7 @@ " min_zoom=r[\"minzoom\"],\n", " max_zoom=r[\"maxzoom\"],\n", " opacity=1,\n", - " attr=\"Digital Earth Africa\"\n", + " attr=\"Digital Earth Africa\",\n", ")\n", "tiles.add_to(m)\n", "m" @@ -896,120 +461,30 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'tilejson': '2.2.0', 'version': '1.0.0', 'scheme': 'xyz', 'tiles': ['http://127.0.0.1:8081/stac/tiles/WebMercatorQuad/{z}/{x}/{y}@1x?url=https%3A%2F%2Fearth-search.aws.element84.com%2Fv0%2Fcollections%2Fsentinel-s2-l2a-cogs%2Fitems%2FS2B_36RTT_20191205_0_L2A&expression=%28B08-B04%29%2F%28B08%2BB04%29&asset_as_band=true&rescale=-1%2C1&colormap_name=viridis'], 'minzoom': 8, 'maxzoom': 14, 'bounds': [30.155974613579858, 28.80949327971016, 31.050481437029678, 29.815791988006527], 'center': [30.603228025304766, 29.312642633858346, 8]}\n" - ] - }, - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/stac/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/stac/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": url_template.format(id=sceneid[0]),\n", " \"expression\": \"(B08-B04)/(B08+B04)\", # NDVI (nir-red)/(nir+red), in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", - " # We need to tell rio-tiler that each asset is a Band \n", + " # We need to tell rio-tiler that each asset is a Band\n", " # (so it will select the first band within each asset automatically)\n", " \"asset_as_band\": True,\n", " \"rescale\": \"-1,1\",\n", " \"minzoom\": 8, # By default titiler will use 0\n", - " \"maxzoom\": 14, # By default titiler will use 24\n", + " \"maxzoom\": 14, # By default titiler will use 24\n", " \"colormap_name\": \"viridis\",\n", - " }\n", + " },\n", ").json()\n", "print(r)\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=10\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=10\n", ")\n", "\n", "tiles = TileLayer(\n", @@ -1017,7 +492,7 @@ " min_zoom=r[\"minzoom\"],\n", " max_zoom=r[\"maxzoom\"],\n", " opacity=1,\n", - " attr=\"Digital Earth Africa\"\n", + " attr=\"Digital Earth Africa\",\n", ")\n", "tiles.add_to(m)\n", "m" @@ -1034,36 +509,40 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "def fetch_bbox_array(sceneid, bbox, assets = None, expression = None, **kwargs):\n", + "def fetch_bbox_array(sceneid, bbox, assets=None, expression=None, **kwargs):\n", " \"\"\"Helper function to fetch and decode Numpy array using Titiler endpoint.\"\"\"\n", " # STAC ITEM URL\n", " stac_item = f\"https://earth-search.aws.element84.com/v0/collections/sentinel-s2-l2a-cogs/items/{sceneid}\"\n", "\n", " xmin, ymin, xmax, ymax = bbox\n", - " \n", + "\n", " # TiTiler required URL + asset or expression parameters\n", - " params = ((\"url\", stac_item), )\n", + " params = ((\"url\", stac_item), (\"max_size\", 1024))\n", " if assets:\n", " for asset in assets:\n", - " params += ((\"assets\", asset), )\n", + " params += ((\"assets\", asset),)\n", " elif expression:\n", - " params += ((\"expression\", expression), (\"asset_as_band\", True),)\n", + " params += (\n", + " (\"expression\", expression),\n", + " (\"asset_as_band\", True),\n", + " )\n", " else:\n", " raise Exception(\"Missing band or expression input\")\n", "\n", " params += tuple(kwargs.items())\n", "\n", " # TITILER ENDPOINT\n", - " url = f\"{titiler_endpoint}/stac/crop/{xmin},{ymin},{xmax},{ymax}.npy\"\n", + " url = f\"{titiler_endpoint}/stac/bbox/{xmin},{ymin},{xmax},{ymax}.npy\"\n", " r = httpx.get(url, params=params)\n", " data = numpy.load(BytesIO(r.content))\n", - " \n", + "\n", " return sceneid, data[0:-1], data[-1]\n", "\n", + "\n", "def _filter_futures(tasks):\n", " for future in tasks:\n", " try:\n", @@ -1071,6 +550,7 @@ " except Exception:\n", " pass\n", "\n", + "\n", "def _stats(data, mask):\n", " arr = numpy.ma.array(data)\n", " arr.mask = mask == 0\n", @@ -1079,43 +559,16 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(1, 128, 128)\n", - "(128, 128)\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAakAAAGhCAYAAADbf0s2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/NK7nSAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOz9TaxtS3IWin4RmWOMOef62fucOlXnuOwy+F5d8fN4AskYY5AQQhbuousGTUA0XZag6GAaIL+ORQsaGFrItCwQEggJJOtJlgCBjJBM4wl45l24XChcPqfq/Oy911pzzjFGZsZtRERmjrn2+fG7GNe+Ximtc/b6mXOOkSMzI+KLL74gERE8jafxNJ7G03ga34WDf7Mv4Gk8jafxNJ7G0/i08WSknsbTeBpP42l8144nI/U0nsbTeBpP47t2PBmpp/E0nsbTeBrftePJSD2Np/E0nsbT+K4dT0bqaTyNp/E0nsZ37XgyUk/jaTyNp/E0vmvHk5F6Gk/jaTyNp/FdO56M1NN4Gk/jaTyN79rxZKSextN4Gk/jaXzXjt80I/WzP/uz+O2//bdjt9vhh3/4h/Gv//W//s26lKfxNJ7G03ga36XjN8VI/b2/9/fwjW98A3/lr/wV/Jt/82/we3/v78WP/diP4dvf/vZvxuU8jafxNJ7G0/guHfSbITD7wz/8w/ihH/oh/I2/8TcAAKUUfO1rX8NP/uRP4i/+xb/4ua8vpeBb3/oWbm5uQES/0Zf7NJ7G03gaT+O/8xAR3N3d4atf/SqYPz1eiv8DrwkAsCwLfvmXfxk/9VM/VX/GzPjRH/1R/NIv/dJrXzPPM+Z5rt//6q/+Kn737/7dv+HX+jSextN4Gk/jN3Z885vfxPd93/d96u//hxupDz/8EDlnvPvuu5ufv/vuu/iVX/mV177mZ37mZ/DTP/3Tj37+R7/8pxB5hOQCSNEfiuhXH2GJADnbPy1wLEV/XgpEABRp7/FZgy4sPuvnbAI6ZiAEUGCgiH5mKQCzRn72ms17+WcT65sRgYYBmAaAGcKs7wuAUgKWVa+///D+ve17iRHl2R552j5qKgJeM5ABREIJDBAAAUgEKAJKAl4ShAj5dsJ6PQAA4jEhnFdAoK8BgAzweQHWBISAcpiASEiHAfNbA/JEQAE4CyCAMCDdtZLoNQ33GePLBRIYD1/d4fRlbh8jQFgEu48KxlcJeWQszwPSROAEhLmAc3cPAkgglGBzEfRzIcD0ImP6ZAaKYL0dkQ4BIIBX0essgni/gs8ZZQpYno/IO0aeCMs1oYyE8aXg6tcWhFNCPkTMzyJKpHo/QkA6ENKBIKTf6y/756CfyQmIJ8H+OyuGVwtIBHTOoJwh04B8GFBGxv33Tnj5vwBpL9j/GuPZf84Ic8HyLGC+JQgTwiwIq81DEX+sAAFCpO+d9bPDUjC+TOA5I11FnN8eUAbC8FCw/84MXhIoCZB0fco+Iu8jhAi8ZF1DAqDonEsglCFA4sU+EQFl0b9hQhkDhPU9wqyfIUSagCAgHwashwgQEE8Z4WEFAH3dwBAm5JFRRkKJhLRn5AmgDIRZ7y8sguGYwWtBmgLWmwAJOt/hLKBi+8cWmL6HzmEZgBJ1joaTgGepaxQClIGw3DDyaGs36f/XA2F5DpThAqAStAUQRNchAF4IlPTzyyAQ+9x8nUH7BGLBMCWEIBhCxm5YEblgzQEPy4icCSUzStZ9UjJDEtsD7z57ZdBK4EQYPmGMr4C8B47fmxHennF9fcYf/p7/jN9x+DV8sDzDv7v7Hnx8PmDNAWsJyIUwcMEQMpgEc4o4rQMEwBQzhpAAAGuOWFJA4ILn+xNuxzPu1wn/5dtvo7yYwGfC9BFjuAfycsav/J3/F25ubvBZ43+4kfr/Z/zUT/0UvvGNb9TvX716ha997WuIh2tEntofigC5Mz6lqPEpGXoaoxmNADUcZH9LejB/ocFbiHEDObIaGeIAxNiMn783UzNOF++DIvozDvq+HAAe1fhIuwVINENWL0D/H7j92wyNhBFpf4V8M2yveRXwnEG56IHjP89F35YKOCfQvAJSUAIw2HvzmUBr0APpMKJMAZQEAWeQLMAQkXeTHijPRyxfDVivCLwC8awHSJ5QD+/ppWD3SUFIgjGtiGtG4Yj8pT3OP6DXNdwRwhnAAmApAApoIPBEiAOBGAjQg0cPE6hRmwviXQZlQZ4YZWIIEQIV8JUuf95HhB3bIxFwElAqiDGAQwIKMH6ib5quBxzjiJUIAwl4H8ChoBwC6CqAgxoCP4QCAyHZ+cTNSPr1CQNlrwa07AVFJuR9Aa8F8SGB14wyBuAQgYFB+wgegTAAYU+QZwWyAtgTeKfPJ0B0GbqRKvr51WADKL4Ej4J4XBDPC0KK4NOAkgnDKWNISY1ZLkDS9VvCDmU/gpgwLDPC8QSIQMYBMgSAgRIjwNSMMgDKAk5JjW4IEFYjJXGA7Lj9rRkMCoQ46EWGsYCh+7yavqJrQVZCnhjnQwSirQMRcNb/D5TAksEcQRRQmEBREEZpzgz0meUrhlzrsyujGipeAc4F08mdSP0qTKBMKKs7JfY+I2N5JpDd9iyRAEjUz4wPjOHe9nSwLwYwStvaJJAkkKFAhhU8rSgEzFywkoCHguuDgEk9hCICEcJ5ZcwzQ0pzEkom0Ps77D4mM+IAs4CJMIgg5RGvjtf4xfdv8S+m341SGDkzRIBpTHjr5oQpJCwlYMkDshDSMiBDzxTazbjeCyIV7OMZh7jgZjjjR27/E37f9E38/9av4G9MfwzflBtgz8jMoGtCNnDs81I2/8ON1DvvvIMQAj744IPNzz/44AO89957r33NNE2YpunRzyUESLBdR6SbiciMFZnXBt2hxDVa8Umpy4hZIy2mx4bq0pB049HkcmckuPt/YYC7SKlGUhfRHtPj1/e/98gpf0bE5/fffS+BIWH7fiKAZD85txtWDRzUiKesXvCaQLPNddKfEwKENApjlBoB+mciEMpAyBMh73SjUiE1GDtC2ushHY9UvX5k/SIxj3IvZmzUyAn7htf/N0PtXwSB6EEHixbOCZSKvogJQurVC1P13oX0UH80WA9YpAwqBSEQwjIgD1TfowT1vj06FDdG9vkoFqQy6ilL7h8FAKN57ZmQR/V2QQyZGSLSnp9FGVQIVKQZHhGdi808bANdwAwlucFqc+nPXKPrAhIGp6KRT1bHj7o1J8EifQFoVQ8aQ7RozfYdaDOfJAIqRZ8DAGS292KUSHV9Upb2Gb4c7T7r70XXh68TnZPtY+uNHlWkBPW1wvoavU5dL8L6PCSQrq/YgRW2R2o0WgSca4zafbDNbbj48aAGBwLIkdX4V0Rh+7cEjcxQdB2UQhDRzypCKKIO0hASgk0yk/8OyJlRunNMEMAZiEdbd10ESRmgxJAiWNKEJYwACygIiAUhFAQumGICErDkABFCFrLrQv38wAXXw4y3xwe8NRzxv4zv4/8xRgz0Pq7HWSPIICgDkIugfEE6xP9wIzWOI37wB38Qv/iLv4g/8Sf+BAAlQvziL/4ivv71r/+63kuu98hXV0hXA8rAiKeE+HIGUgEtK2hedHMxQ0JpUY1NDoWwjXJKAS4O808dr0v0EbWfE9eISHdQ9/P6Hq//LPL3COG1v8frPA8iIARIDEBgyBghkVGmiPU2Yj2wwTt68OSRkfYK8YWzQluURCOpOeumXJNFmwQZIsph0MO8RFAZ7XPNCJQGs8oQsN4OSAeNoPxAAGkERXaCDA8ABBiOgnguCvlERr6dIANjeim4/s9cIxCxgyQPhGSRD2Ug2POrG16oGoG8IyzPRlAWlEHhIQDALAhz1sNmLhgsUuDVozG9lzLGep8AUKYAXgXDsSAseqhzFmQ7DAWww7A9ej94/YAD7CDrjEY1Ln5wFNRo2KNDKcB4L7j6lsJNEGC5sRdSO+xK0PeAAMWicI8u63OA/y1huY06N4EgUaM6SowQ2YxXswAlMvJO4Ta+GQHctOdOaPD3hSMlTEjPdwalsUa1kRTqXAtIoP9f9XVlDCijvidnAS+5OVI9KmHz7ffHGQirQ6iCMhCEA4pFZVR0fspkUdoiCLO+R1gE472tsUnXGduzyyPXOdZ1SCjRDFr3HDkJ9h8oBOnRMgCkPZCu9DPjAyGe28/LKLavCOzXItQctbsdlmFCPhTs3jnhsJsRWBBIIykyQ6XftzmPUeG5NGTcvxNxHwZdR8GcGhHwSoh3vInu00GAr8zY7RfsxrUZwM7lKYWRU4AIMK8DjmvCYOfdOUd8vFzh/43/J/7d/Al+dXkLH9xfA9k9J4WFLw35p43fFLjvG9/4Bv7Un/pT+P2///fjD/yBP4C//tf/Oh4eHvBn/syf+XW9z/p8Qn5nj/vvCch7YPok4vpbEfGUEB4i+BVAKQMS20br4ECIQX856/mQ0vYDLo3BZQ6oH54v8rxQYM1JAQCF1xu11w2PRjyquvwshwR7H9l+JkOE7BR2WZ5PSNcBJRLWg2LnvALTXUEogrQnzM8ZeSRMLwqu56zGPRXQedZ5S1kjzBAgU8ByO7SIjABKgvHlgnA/a7RQCsCEsos4vx0xP1MPWVg9QwmKg4OB4R6YPhHwKhgfCuJRDUaeAtbrCBLB4YMVN/8loUwBr75/wunL+l55p4cEJ6ixWEXzEjuNamruAEAeGLhqc09mQKgUhFkNSFw0b1GfsQjAjLyLKFOARM1/SNT7CHNBfFBDRmbcKLunjpo/g+ihxQnN+zevfd0zytgM68ajdo+/qNMggTU/CGD34YrDtzJAhLvvn/DqBxhlFPBMej9FDzv3pGs0UxRqDR6NknrBEoDz8wB6FvR67T4oA+UY1LXKAoLmhGRkrAdWeDJGpKtQDX2YdU/xkh8ZqXIYcXpnxHpglEGh3hKB8ZXg6n0Bnwt4KQgPCyCCfDWiDAMIBFoLwnFtz8Z8PhkjhC3HVgQkBE6CeCwIS7HcF9fD0HNx68RYbnQtDfcEThmcBMODqONEQLL8o4+0py1k6k6BR2BRHYX4IHj2QQLPosbV1tX5SwNOb5thznodEvR9016fU3wAwlnnPh4tt1aNB+H4PRHnZwH7m1QjFzZDxRAUUDVYRMA0rHi+V2v4bH9G+iojcMFbuxOejyf86sMz/Jd//z24+ibrXroXhAV4+B7G+T3Bl28e9DMgSIVRhAysEJRCyHMACjDzgHvWiOtuHquh/HffeQ+pMFIKmB9GRbcKPV7vnzN+U4zUn/yTfxLf+c538Jf/8l/G+++/j9/3+34ffuEXfuERmeLzRh51IaUr9UjCrNASJQYNARwULoGIGo8eLguhGacQAE6faXwAqGF73c9tPCJFVOiOX//en4bFVuPWwYKXoXGfe3KDFpRgIZFRRkaa/DAxzz13MAgR8kDIk3rl0r0fpdyMeXe/FQaxfBj7dVjS3N9XvWWqnjDQogxx/B16gGs0YknsgvpaKsA4Z8RPjii7EeG9UWEusQM2CMSiJUotQPWcT8W7uijMoysqsvHiKBXQmjf3K9H/r4bPE/QBBXQyz/5ThpMkyP7dDJRBTDUBePHVP94uGtPv9X7DnNUpEEFYJ32+A2ryvX6+5aT6+W/Gm9p1ENRYkkVfK9X5kR5+rvdm+TOLFLLAIFxAkh6+ANSQXIw86nrLox7MJQLxpNdQI6RUFBbs4CqF68pmnUmh6ky0i/M5VuOQRzaj0jkuNiclOJFGagSheTONaoLBtiAyCBAGbaOund5IOXxKAsT7jHjKoDXXyHAYWR2T2NZC8WfMAik6n5zcERLEWRqxJgPLswApVA0TABR7wG6oNpEVCwbOiFywjxoRjZzw1f1LvDe+AgD8l/AeOAO8mBMzC8ISQAAmI0MQdQvRp1rIUgIKRRahTR5MhDCfB6Q16N+sBu+SnsXf9UYKAL7+9a//uuG9y5H2Qdk45tmUAViuuW6gcKdeHpjafl8TyHcSEVB0U0gIm43xuiEeLQVWI1eNiOaZqDcqMQKxOwlfY2TEiQ7mCVbYpGe9+TVVcoRCiuLv3bEZZYwoO4X5FKdX7zLMGtaHVRBP6vEKE4YjKTwyy/ZzfOO/LnKUdlBAgPVmQN65N51BqSDvov5dsnMzt9c2SK5BJnliUNalmHeWl8lqIACAcsbu4wSSiBKgEZPxQHgVxFNGLow8hi2DzqE0//hgML8oXEg5grJgkM7okOepuEE55innUQ/kmj/bPEvCeiCUES0HR+o4hQX1AO2fq+ak7NAPig6XaEZaCDIyCjSPRkUjqfpMCjC+TLj6b4MaKc+ZoN13f+/VWNvhq9GHzf+gzgdlAOQ/75wWRx6gB7nDhpxgJBM1nmwwMZlzsFk7jPo6XT/2bAsQzgXhlBRmthyXkEGPF/uh7AeUIZhxNfLOwNUJ4WzXtBYEWMTNbb0CDvGJRvQn0Sje829ikDNQUQNOAlrUoOWxoQMlXka/Pte2biLXSJYEiLNAVt1v4Vz0/mLQSLkA4ytYhG7ODJEasEAo0Py6FMKSA5YUcF4GlEIYx4Sb3YxAgsgFb12d7DIE5zQgcMHNMON6mDFxwj6sCFRwO5wR3znhfj0gzIT0MSM+APNbgmm3YggZaw44p0EjosI4LQOKEFYzPiiEkgkpBTBvPZP9fkG8zhAhrDkgZ0ZaA9a7ETQzyhdhU+MNYfd92livGGXfDr48Acutwj5hCRgjA0UjCydYMDOE1hZdFVKsNMYWsXwKHZ3cVY1R6eFOC+8iH/F/h6BGyhmH5eL9iDTZHMwjyfkx1l4EQh7RUD0YZYgQp5QbzCbMKLuIsguVEMAZAAS86v/DUjA8JNCiCdxhR+CVMJxKlzfReyAACKJz45csAljS3iGX5TYg7SI4Cca7gnDKarREoYPNsFDfD6wSdVYTzAEQjeryQJAEo8UTKBXsPjhi+oiRrwbcf++E804tXZgF4ZhAJWiupHq2W3YZYLCMGTchQokKc/BSEO/bzxH8ILKo0AxUmqhGCr2REtLrXq8JeQ+s14L1tgBBmZA8U4tqLGIZ7gjD/dYLL1GvL48EEIPXgOrmFGi+MJVqOKYPT4gPCQiE9RCRrjSv5BCSbKIg0YgitAPbyQZ5p9GURmNKCOmNFEqpZB1nC/qhXmGtOSMc9YGrk0SoeSqosVD4kjYkB8pAOCXw/aLkHCdikCIlws1ZAQN5F5Guh/qMS2yEFfZrWgp4yZAiiOIRYXtWMZDSvRkYHjKGV4uSgTxyGgJwHXV9CjCeBeGkjh0ZXFlCe1+RbeQrbGsIaLmwrA4iAIyfLBg+Odm5dAOhACrA7oVgfJUVor9mpEk9jRIE5IScTJjXiIfTiPXFDpQI81UC3lIq+O3ujC/v78EQfHS+wifnPZgEz8YT3h4fMHHCgRcwFbw9POB3fs+38f7NDe6OOzxcHRDvGOvzjOe7GSMnrDnguA5ImbGkiGUJKIVRFjNS0GvKiZG79RZjwTvXD/jeqxcowlhKQCqMl8se36TnWB9GCH06GtGPN9pIebgNtEXi3keJpJslmYGyug3JpMQEN0YGk1Giyk4Co+PoXuIwBIpRjZrDIcxbaMSht6Cnknq5F5Bd57EDxR64vJbV19g4dr2bfJXxiz3PYAeDf6Z72HqwAMgCTgWlaK5EapLdjXOL5qjQ66FKh8w8EhphdSsEylyhskvik3vvRlTqDApQbMEXZ51VKIUV/lkVPpGoxqw+96L3I4X1gMqaaylkc2WfRaiEMoMr9dCucOlnDfJ11T63dygqO8wiojIAMhUgCIoE1BoZFsvPEcKpGae6jj366aIgr2vqaFS6nkkdi3Ba6/Mqk0d5j+/ndczFGtHY51H9fGVAbp95W4PkRE6DT53cgSy2dwDE7hqcTNGtwwa92vPxusWLeX80KkxHRopoc/a615HnsGqE7XNk3xuLVT/fSCMOKXJzDCtc2/3fqf1E6tMS/H6kwZ0VRkfNW3IqwLyAUlAjv1pEuWhezjfGBrouvogJyUgLtBJ4IeSBMc8DRAj7IaCIQoIFhFwalD9Q7mBC3QyHuOBqXJAy4zzsde+ywohLiVisRioZLb0UhhRSZrAbZlHIb+MS2TUMFj4zFSQKOHJGCIKVBeDXLMrXjDfaSIVZgCMqLh9OgvFeD18qgvV2Al2PHcQBhBMjMBu9OtQop8JtgC2IosZoNym0xqyRERnleAjVy6clPT7kI+vfAOr9XnyGBAHFoGuwQGHI1MGQgEZXy9rqvXzwNWQ3akSSYPRopeB7zoHnAl6lMpGU6FCU0Tav4MjgNUIYlrBewXNXIMyOz/R4GTqDpzBEHtRzV0PHmosy2OrR6A8HQDcDULF+vXfUBHXaB4TbSQ3ROWneyP/GDnZeCvionvAuGYMvWsK8eu96T+t1wHLNNQ9Rv7JoLkQEGAhyeTpKd13kxtaMYJZqbPOklPl8KAjXCUSiUWJSl7zsM2iXUTJjDhFl3ILyTkv23EiNCJgqhLtcxcquG18mjB+fgJJBawQlaF5iFcSTGpoauVpxa5z1Z3kkFDMeGmmj1bElhabYiDQoUtGCcFyx+0gRAIX+7Cu1gx5UIEk/F0OolO34oGuyDIQwa750vCsKr11PlVnqeyDMulh40esQZouAqa69Ev3ZtGhpfj4gHLR2L56VFCMEhcrt+VWWXlAGp0NV+jNbx1F/sFxpfrcfXjzuEbJH7sNDwXC36nOLjGJRJa9a+wYRjRrPWk/Ia7HzCpXpKDEouWTvnpj+pwwA30fclyvQKWB8qUiI3DHKdyJmBv7b1S2+efs2OBZQEDAXxFjwsJ8wlwGrFJyywgkPacJ3Ttd4dZ5wf5wQ7gKGlwTKAd8Jz/Hx4Rq4yH+Jwd1YGeE+gARIDGCHjSdUCuGDu2u8OO1BpIXIgQSn1fJUj/DoTx9vtJGK5wLmAs5qOOIsGO7Vw5RIWJ7HTY6CCjB4vUUR9c59I6YMKhG1wBcMTCPK7QEyDSiRlenVQ0Gk1fDxbm4H3Zqqt1tGZanhbAaoM2QkATKal1aUMo95qZEZAMiyQo5HIGdTrVCvP+wm4LlFO1AcX9iMlNihvEjdQGVSlh8nAc0r6LSAQwCvAySwbuT7k26cGCCjYWI909Cn0YkoZmyUqWVeZSBky7/09Nv22s5zli5a6lah/75ANyk9G8FrwVAEwYxUX9/DSwYdzwgnAt9b1DdElMOoB+mawWeFkOI7B4DGBhFRMwqUvdjbNo/R0H3fuSHSb4C+bo3Mcck7NVJyyLi6OiNwwSs5oMwBQoJws+LtZw9ImfFyOGDeDaBM4DNr0ehC5mkDvGiejOeEMkaUSQ/n+XnE6R2FnG7+GzB+WEDnFXwYzRiTlhmULkoNCoXFWRDmovDl4Ae20+4JvAjiST37cCp1f1SCggjoOGP0soQxaqGxqJFCVnYnMkGzKB0akQXDveWbAmHq4NgyMso4qlEcS1WlCGc3UkrkITNE1UiNLTfpo9jPIazMz7mA16TIBrrAxpw3idDcX6Wz69lRIqoB9LIFzZ8BYTVW6suEcM7NAALgOasRyhllNyqCwwAtRaPeVMAPJ8isqiJhLbovC7QEJBVQVtZtOugxVNmEARheEeQhIpwVLuZVS0iGB32P5ZYwP59QIrC8JVi/tCKPGQ/XI2bF1/GQRpxzxDGN+OS4x/E8Yn0YsX9FmF4C8UwI5wFljMg7wXqTgaHo3Fn0QytheND1mq2IfOPPFsL9yz3uVr3/sEuIUR20vHKLwr7AeKONFBubiLJ6kJcFfZvhQVIk5L0m9nnpihaJQOMAx+CpiOZ+Rq0hEavtEKaaSAeAsnoequjn1yJe/XdNbfWRVg/fAM0YdIXJ+h7UDodStEi1G8rYsuv1sKSbA4dutn8LeE6jGo0KURTAsyCWj6mQUJeYbx70BXzTGSD/2Qa6M+jtUaByAddoAt+u3XIPYIcyG6QpDJXI8efWf9mceCEuAIWjKkQBeBGvdPP8OHKUCp954Wj9e7/2ygQT+17pwYHFJLeg+UyDRaRS7uz9gqoFyIAGAUaVDgL0HsvQmJr1y4pqK3Tcr4vSoDyGyyBJ3SucLZrPjennz5JrkawaAxKL+KVFVH7vwnpQCRHoNbCpPyt9rp7XMriINe+YJ67GJ9j7OfGnOgT+/D0P5HPnjoavv8thkZEbZDEPSOvF2ppuz5JbLtSjJLtPdvJCR/6ByU556qDmduvn+9rxc8Uukh8/N2FqebbQ1kIP//rzomT7xJ9ZciYg1bovSgBWRiHgtEbcJRVEuF8nzDliTlGp5Q61W060Ml+NvVfh6uqxoc2b6PMsmZpTInruyRKApM8rI2pUKVDZJiNdfJHxRhupMBdQKABxTWQuN7og40kwvEqbhSuR8PDegNM7OjnhbJAhoRbmaZ2FQiOcTePLIrO055YzMU9wstfwWsDdIlXIgNuiTfmCPBGMKh5AUSnzl+QKWjPo5ko393mGnE7td+ekHtppBs4zEAJ4mVByhARGula9vLaA7TqCGkOxa3VGU82tBYYMUd97TUBK6hmvWWtPyDTy5gReMq4YGO/Vm2Y/CBMMxhCs1wGnt4Ox3VqRba0rAuoBBTKv3gpRHduv6hCmCeeHRNoT7r42Ib4zIp4LhlcqIwSgbn4GABns+XOlYpcAy6EB6SqA39q3/IpInR+vt+GstSFChHQVIHGqpAoJwHpln5eobnoAKMeI3Yeal1qXCR+eFOqgY1BILgryswy6WnG+G8CzRrcQgN51Bmc7H1xPLsx6mKxv7cHLiHSItRBX652gRnlBrQ8KJ9X5k6BFtMK6ps9B13WtOzO25/psAnCh9GLOjhAhHQLSQdf4/kOq0agXkm80+nJpkG3NxwLpnT0evici7dwh0vvbf1Rw+Na51hmV3QgZGGkfkHaWgwseCQK7FxnxuE3ECwF5Cki7oLVtx1XrwEJzNOOD0cQFKHuDUkM7HyqLL+q1pb3WeMUjqVTXqc0LRPQgvrE5M4cWMMfOok0ZB2AcUKaItDeGYgTmtwKWW2UsLzdkhe9qbLgA6EpIOJsBi1ZzNhrxaFSSjwRlluJFQAkBH6dn+Df3WpCVU1BDQQKOCgmGfcLy2wvmzEqWSir7JEMB7TPYSj7E5JKIgTyaYVwI+eNJ/b5F6xcB8+MIQIHS2pOu9/VaIJOAli2E+mnjzTZS5wzmrIdbIKxXyrAqwcQl79eGcUMX4fx8xN3vWAEG+CEgnHRjp1vNF8jCiC8iwokRj8D+24LxQZANI3b4pBUIMsIpIASDiBbVelM8mlpR/EWhMAFqFIagDkUlbKBGWQ3rF/DDCGLW92FSeFAEmBfI+axR2Lo37F4Pn+WGO9adYe5sRcbsnnXzmhHYcm+K/VN33bRmJSgQqcLESRMZuzlh+pC3kGAq4NMKrAn8lRucn++1LMCjCo/C/OB12IXUU3Tr5RFBvb7I7SBmLQx+MOWJ4Z5x9QEjHhW+DWupfBSiaPAi1cNEWCEzRGWJQhQui+esyWvo39Eq4KA4PSziWA9an6esP11v6xWhUtPdMxWATgHTx+btrox1VoMZFgKtQD4AfL3gB77yEb59dY27++cA1BDnvdYrKfxGRvkGeNZoRxhYbyKohI20kF+HfqYpOhQBn3M14j74dsJyPYJGzYPFB63xSfugShTmvHnOkJPCXUJa7pEOapjjOSLcW852UvRBy0IsN3jWvUEnZQAS65qReMDpK4T1RlrkUABKjOv/I4NPq7JW9wot5p2yd+t9il7P9PGC+OH95t7K7R7Hr+6RdozxDhjuFO6uSlBdrqgxNEONkuIsZuhIxRKCQlteVybmWLVrEVPkCNXhrY5gJzEm+1E1L029wx3f9UpJRyUC6Qook4ASIa4tb7gBU/yaRkMbuogLAHhRQyUE5FNEfqHHfTD0qQxAeiuBrldM04rve/clvrR7wEfnK3zzk+eYzwOYBTFmEAFpDUgJasQIKKNG4rwC8ahITjzqFxhYr5U5Siuw+0jPoXQgPHyVsTwroOW3QCS1ybt10BXD/y0bZlSDD6j+rLLEMkGShqCuL1W6RVihMYIWt11IskAsuppUOkgGo76LCbg6nPToJgSEx3Rp/VCDuQA43b1KJlWShyW2Se+zL9r0mhaFj1hVnz7NeamQI3dMNtpAkJuCX8/hzNBo1t/Dr8mIIG4EGyuqQQbkbOOguAoBlSbOFl1JJFQQsqjH6xprAKrquT9fP5Ap2XykC905Rs0n1oLbjl3Xz+2Gr0Ro7L7ux/W5sXrbJcqGtbRV5SZk91UcOvQpszftdQnF6nCkNI/e12CFf8wYEQEKN9r7kBp+nS+Ga7ZV0tnF+q/D58Z1FwdUFqdfeEUnyD6nFgN160NUv89ZaT3UXEs9+mi6m0/qn8NFrd4G5uuevcOuFVL3S+yvtaIIUEPIYsZJ4T2vfxLSyEXTAJamzr5YLuYP3fuDLEqznHWwM8f3Up1jrmSYuvaM4NIr9XsqYwOle66X2rqt5w9QIel6LinQhJKosQtXNSxGg22ABgkiZwwhYxoSSiE7Frp7dZjOYD74Zxj5o0LiF+uqalqS/h0njda+yHijjVQ2ZWhAF008FbCpEg93Ca5OQLYhaM0YXwnG7wSNpEy0lASgbwf1SAdgvRHknUDOphq86PuGRR9WHjSkBgHjvbJ5KBcszyc8fN9OGXNW6MgrIR4G8MOEWjNlG5XWDJ6pGoge9gVQmUZwNuFuBOXY6O6XG8Vwb16A6cWK+KCQYzqoTl88E+IxAEYkqAQGgkJ8gNKbzSOXaQTdXAEAyt4kkQTqEb96UIz9Unnjgq7OS9JaptkNhP48noHpVQYlYLlhLAJruQAsz0zNYNQEf5xFFcHvzuApIh0I85cKwpmw+4gw3AvGO8Hu2zPiy7POg+ehSlHSChHKbkAetRVFGbA5dHw+eFEJHiGqjgaMXp92hAgB3xsMbEl7pZ0Tli9l8NWK3S4hsCBlRjwSDt9RVhu4FZ/nUSB7oAyCsgZ8cHeNeR5QxoLldjuHEqB1TF0QRIlALwXjJwt4yci7qB581Oe97vXQTTtU2He8j8reK670DosIUSPNPDGEIpbbgNPbnWqIzdVw7ByOomxA9vyIGSJeC8TgZTaygzoPHVvU9gAvBcM9ABhZYdQcYJxNBSRlUApKCop6+OdB9zSvGkVBgHQ9AHQNygpFUyookUFFNPJjwvJ8qg4Mm+OSD1HvOQBp4vbes8or6aHKYBMBFgZyIfDSPQ/qCo8tpwRSx0PISiM6pEGiSW6NjUVYc112iMcTIGetdfTI2VmXlHVvrFfqiPAK1FYpHcux1obVvLDuq3gEwgnIe0K6UeX3lALulgbtvntzB9wA5zTg1XlCKoxlia3lx5nBsxqqMGvUBugc5VtzqiZ19mkAzoGwPCdbu8D4CSHPvxWM1MDg6BCO5kqGe/Okje4JAE48QCKMDwXTJ6EzJAAlwfRSMN4XzLeMV/8TV/hBMVfzyg16UnFW3TDxmKveWPqePR7eY9UkuxOMd1AVgSkqDl2Kql34Zi1GufUFdHmDUXNEMK+LxqHR0V8TlXn0JKUgvFoQAOSrAevNDsu1bpgpcisQBWrC19sseF2UEIAhANYKpUyxMQTXDHl4AIpAcq69uqpSRQigaQTFqLmsVcCLbcYRFWYZXyRTepiQJ81rlVvC8qxzj6WDVk8zeNkri+42AxS0GPleML7MCvd88mo7KSXrNRKDvnyrEN3kB8KFVwAYDXoFMaNwrBGzKmOg0oXDUkBCFqUoHDY8P+P5Tav2X5LCybsPF1AqWG73mJ+pgZQ91BGKAkmM48MOJRMQRXsJZQItlheARmlq4KlONWcgvjyDTjPoZg+SCWUQrFesqhwjMD8nrLd6wKVXhPig+bUwqxPlHq7XreVR3fq0ZyzP9D02pQd2KDl5hleqrU2qbNGaNafR7b9eGaXfl2EtiEc9XMsIZCse5xXGvM2QLLpOO8YirAaQrY9T3hFKHBSyvNc+VUqT1/0rEVhHz1cX0IPulTwy5mdsJBR9f8rKlI2n1A59UeXyEvX6w4KKzIgRQByxqFFUhkZH7OQqvWfXFKwGKrRIw1nIXOcYXR2VnlG86jMuoxbQ8ipN1cTJO2hzJQywMVopozp160o4LVpLVTLjOI8oQrgeF3z/1Se4Gc749vkGa3kLx1kxTlq0NotXg6yzw4p67eu1FrP7XDqhKB30ZzwTpk/I+kk9OsJeO95oI9UX3QFGGS3KMuICCCttVYB6EFfmjKFvRQyjNQqoioFefhDa50j7vtKXTfeNXWy0bmiDo4BOlaIjRxRRyKGQQkS9lwk070hQo6QaiXmtTj+cQdQzvSoTr7VOEJNWgkMk9XpgeTF9Dy+iVYPafRaRYgjoXHugET/8930ho9hcJEDISBZVzFQsSqU2X0CtfysDIAMrPT4q9q1kDpV8Us0xO9A+Q2pFW16gqQWwQTSGlfTSUE0aSZ9bWNS7DWcndwhgmnACNV7rccTLbjpzCtjP7jwoISEegTJpzkGCblxkQllZGW+FGowC2GK6gB3hB6Meil6/9/iGu9f4oRHUw2e7cI3WW7SmBk6qIHG14WaofI23PdBo+mDoWvaXOIrhhqmDlP3fxeb3UZGvAOK1iV1ngvp30q6f/LP6jfsa3cFa62aEFzLD5HWVOqemXNEvowrxtX3d8rkWPeYCEUJhm49C9XdVVd/Xlq1D1wOsTkBVM4HVyqFzDloE+7rRw4D1y6A4QO9R1jbHdRSoYgQYD8cJ52VA2gd8eXePm+GsBblcMA0JD1wqIa9ei8N7oVsTZpzb1tJ/SLcO8/iIJ/ap4402UrwUIGrYWqLjpMrgiqeCeGwV4yhAGRnzrSVpGdUjkKAH8nBMVrXPUHWAJrTphIkWSlP1BOn+CKSMeLxBmDVpGk+C8S7X3EiZBnhVfYU93NjAHyT0+ypRpNEfEamu2Wne9pISgaTuVO/79dQ5SognrY0Js+qSFVNK9/YMHExJ2rxg8iTtvIAeTgAzwhgVXhW7d1PcULkWNgOq9VwEKDljHIDq6dk9WYuCeCz18B7uE3gtKCNjPQygL9lCnvQUkkhYnkXE59fI+4h4BoZPAqYXhKv3E/a/dlSixry8NsJEUGE8GVnhr13ntdr0KaRr5BJjpykUpGHc4YMVhw/88JBWzAw9+KYXgvK/jSjDWD82JuD6WwV8XkGpYP/BjOFhwHrFWA8By5f182hm4BjUT/E8CzVWGa8w4VdzxgZbGntSdt9hrHkQoHMI2B0pNI9/bJ65K46UKvzqZA3C8Ipw+EAq+7WvN/QygE1exmn51RMyB9KdFafxkxbP5usRZWCstyZXlvy0a29RDoOyXiPXTr9xLpheNQPozqbe96cgDKIUf7hPMBHSTsk04SzYf1sXfN6rwrvXjnneqn8Pr0GLZy22pjmrv7aoUcy7iBK0ozAlc56c2WcGWWJrVaL3DsDo7R5JeaTb57w5SzUInj/znJRT0N3QViNlz19JMc3QZiu65xWgY9T80AMBM+Gjdw64GrVx4cAZXzncYckRp2XAqzChqnZY8blT1/XmgOGBrD7TGIBsOdegZ+p6a8SQ86dY3IvxRhupsGrrd80RtV3keRZt6FXgTdrK4LpmokYouIGyBz1n8BJrcrp6XmagXIbFKb4kAK0FcjwDSVtw8woUgwPCuVTigAys3tXSRRiAKbLr78DSkssiyqIx+RBaE7AmSEqbSAlSWjRURHMxfSSVirZRGFk3HqEaKI8mNoSBLm9GpxnycFRj9OwaPf2YY6heMwHqIbr+ILMZBr1Xz4HUjSZqFDw6C+eEcBKUMSCeY41OZACyRZjrQXX7st3HcE8Y7pTVFb7zUqm9y4rXDiOglGDtxo2dhc7Z5rXUFuYacTPywMgjIywF08cz+LhUckyJDEoeHQPDQ8u71Y/NwO6jVItih09OiK/OiLc7hP/5AAx2EB4J4dw/U/NMh6Iwn0FglTRhSfY8KX0+BFPWcOkcO4hqPVDxyMTIFxv9QTK9QCBdC5Z3EmiXgf86YfxPBdMnaVO8ng6M9WDQGDo9SbIIvWNtVtiYCQRz/FAgU8B6o7kgb4ehRlNaRCCCMkUrMfFniFpwLC59FjoD+jlnnp8L/XlxOCUML8+gLEjPJggPdd/AawT99fUazVi5en43OBCoKHROuXW/pmSOqeWvSmyMyfo8ukjKuwM8un6DfKtiRve8vZibWKoz6c/CW8Y4pOuFymywcjgRDu/rnqIc8Oq3TVhKBEPwbDgDA/D+eINX3ObbDV4ZNP8EKAs1zB6xAmQlEdRFeHlnhJXwOQ/MxhttpJzJVBePew6C2sMHcC9NIMSIZ1FLzwAlo/Xa2VZi18G2NuhC81Arw6avCSqoHX+7zy/B1b2lHoaVBDVp8SmtuRkEl5QB6mJGd+A5fFGJCh69EOtX4I1aRPUqier8AEDaBdAUNHk/ccWpo+fEHI4BFGoZIqrMU7TiZMtdEQCMI2pn4xCAlFROyk//VKpygF6YTe/IyF+a2rXCyAe3hDwWPaQTlGE3Cc5vM4CddcFV6C2eoDktp+N7A8vX6B9eDn+WbA3yaC0WJRWNSK2oVTxqItgcmNQNO/vNBHFds6+HQ4JKMYUvHTSitmldr1SOCqueMpSoMR27QERmhiRBsEOEExobTDxv02jOziSrUkXFcgdzt37tGYiRPXQfKJwpR0J8EVGGgPEVKRFpcScPljeiSn/fMFtZBXv7+lhXYqg1eZFBWVXy3UAJ2/7rKzTE5JmsI7BCrmLMWzMw/QHtkUTuUAjYPl1K3etS4T0Cm5BjhW47aK6yWC1ilEhVFomkW092f5ulVklQVHPJIEBy1zJQtlGSOx+b0pZi6486Qyz6oRu1FHumJaAqcryWgSjNMfP78L5VgJIgwiwm9wS8fHnArwxfwWBtPogEx8XrDVXaKu+gcKFBl5fMW43oVXsEQCsS9vv+YjbqDTdSgcCiEICs1uVzadGLe/60FnAqCCfC1QcEXgK8l4wTKABtO5H2tngXbeMtJNb6Qws7YVAAL9qRlc+d3h100Qi0nYS2/0DNmQgRyqA9gMJZsP/gBH55VDklM1YyRMh+rPBGHcxqMDYc8qDdhWMAQkC52SPf7PQ6Vms8xwxeC+KDwhmnL0ekHapHLkTYB8L4MQMztorx7p0zIR9GpL0mj2WMcC1DuT6g7Af1Ku9P4POyMXR0njG9b7pr+wHpWrsoH78ScP99jDyhRawsWJ9l4JnBL3MALYxMwIuvFGAQ0EPAzX8MuPpWwfgqgz+5R7l/wKaXV+GWk/DEY58XgCd7FUob7gviq3MrxCaqtPptp1jNj3iSPB2CGtVJoxGfV4+0IcB6G3D/1bCpESvWxyvcB1DWwlCet06OsGm0knqm4ysBLw4B6nxNLw0q9TYjlpQPcwTv7MB9pclqj7xUqcIKQA02HB4EuAd2djBSAca7hOmjuUpKeW0ZsDPj1kcYgjIS1hvvn4JGOfaml0ZAoiJIeyUr5FEdjelVMbi17V+eE3g2GSUjEHn93/xcHch4co1BfV08542hoTVjOK6N9RbcgKDmGnlJqoBu91HbldhSKZEw3+r1clbCgSs6iHXA3pxJXmzuTgAFkAREoHa85jkjPnCbQzIH7YaRRqpIQ7zPConeBGUTozdK6tgEi57Tgeq6jrNUZ7q/D4+etFmnnnvj/dZYUhHsPmSU/+8eH1/tkCdBvi7aEYFEo/sBSFNButGWMi50iwLQZNGcrWWeAYqEvC8ou6J1VWd1zPBbgd1X+70sOoE8F202Zt6X00I5abW7EGF8GarnlyejsxYLPyetCwHQEowethaYFIkaKw3jZRvuSzOOmvA3yRkChDRKW/d6QA1HwfRJaD2Ccq7Qn1jCvnptdZPxYwYgoIdxDJBRaciApQiSe9BiNGXGeoVHFOfh2LGPiKyYF9oOJGteqRjphIzB5DmIchix3qi+XuyS5FUCZ03AWXXKZD2AR1W7SHvC6b2CcsgKaQatSeExYxgyRBTmFwEwCN7+8iv8tmef4D99/A7k/3gL04uk1P+HE8p5VqM4DK0b8qPFcpGry+oxhkULeOm8amTKrAavsCqICKpuY4lUcz8Om2h+Q3M9yYpvNQegh43E1mCRrIpf803KeCSjGIfZ/sbhrm7Es2B8JbUDcTaGZLTmi7R0YQizQryZIUKVIl5zqq5yPej3YRGMJzFiSMH4YgWb4jyf1i1RhwjhMIBTaILAPr1MKBOq5+95EaVXG+Rrey3tuBrJMGvpSDwl8CkpUzbl5nB0UY3nP1zQOFhTwIqk1O7K9pJc1GlaDSJ/RKSw753IcQGZurSZN1bllbRY1R0e1rWxmYfQpJtKtHyyiLF0AWSowzyXGtVAYPlYQzaA2gHa69Za6xS0dIZBuRJRo8taC1X7fhlKEZpjoQLJaqSGOy3eFiYtLB4I430B3vcOyoz5uXZ/TgcBnq2gWIDRtxWhnCJQtBavWPNGKYZSJONuEDQnpYvFFFHwhcYbbaR8UdXwnLUBGrEdlElA4gKyRXsTzhnx5JRTbrVJ1OG0BpFowZt7+eZ5Bii8JlBGzxAU3nJV7MswVtprWwEmzJAxZGdGwNSSxYVsbWM78YIBq5EyOGqj2l40p9WpUofZDi+H6phAOVZPrHr2Dhl0skN143m/rMvN7WQUURWBcOZGUBmiKcvLY/pODEhXEWmvFP4yqnAlCgGrCoDmlZHZCBrJWqiI4O5hh2/SW7i/2+H21BEd5DXwHrC9ZlFPoRa+mrPhzylPAeV2j412X5dPgVOGY3u+3r12vVLlixKU5uxRYTa8PcwqnbOJkgigkVDEKvZTIzHknUE3pREcNtqGYkxNO4nLwOBuG7vOoT5nASe9by0fAErers+qg0hNMV1ID1d1Rhi16aFBX31XgQqbVuktRx4coiIIKzwelqJzL8XU6q3ux/NakVH2AyDajNIjnE2RcFZoEnbIUtbcDSdTjgi2lwKhQEsg6kro1u0jSrxBZe6AqEW0jy/dVyWEGIx5affElMxzywf1TNxey9C/dw1JTqperzJsQLqKLQ9en6/Z4C5aLwCCsee8dqy19wGcXOE/czYjJ9QeXIDNJZpBrPn3BSAh5ElzgSEUpDlCzooEKAGNqnNQokdnWohfRqAMGoXVhSPt7z9vvNFGSgvcdGK9QV06qIsXjxnhbm7JzVUP7FAAPiXIwFhvR9B1qAWCZTQ19SNqrooMc82sCVdAN3scSYsM14Lhbg+UpvEmF5XUKgjaJTGN2pv2AfTWHmkXcH5b9cvYpGzcO4znAhQgngOi5aH4nFRFOYsaBKtT4iWBz1E9yJfHysyT3QjEgDgGkIeKRTe7C4+WkUH7ocIqYKDkgLDGWqTYL3QAQC7gu5NK3QRG2Y0o11Hh1QfR5oo+iFAOI07vRCzXhPltgK4ThilhOQ6gB9X2CmeFvwCgjFKNez4e8FHcY3jF2H9UEO9XJTKIKORpB+tmEAM5QdYE7xwcTza3XTHmcsNI+6tKawZ07sNZczK1IHpihFUw3Gv7hzwQzu8I8vMEWIGjJ5JlKKBCGH414PpXFZLyZLlr/eW9FVeeFA1Ie8LyXLDeZvCZMb5gTULHpurgSfX6CPZRcwP9evM2GllhZRKtB6pkGUMHnDRT6dDmbRNr5ExsJKTuQM9T0BwctzywOkWlykm1jsatsJUyEE8JtBYlyBwDykCm9l6ALChTUMiQCPGUEI4JPZQOUWh/uNcDbjgV7YSwauNLfpi1c++V0qiDXQ8vqZ0DosbPuxKI0/cNoqwHNAHOtK20907hAt2Zs1lySTDcrXXNV8d0zeb0KUzoBtFVVQCFncdXGRIJ61XAco1N3r10Ekja7NPa3YemCBLPWoQMkaoaoq9vDktlJmYv3dC16urz0kVvCgfresk7AoaMabdifTVi92tR2dEGbyp7VIUQAEAGdQi1LrBg2CUkDpCHizD8c8YbbaS8/sAbjpXuoYBQG+Uhd9I4ouwVWQPC5FXfAMa2oYIbieyerC9i/2BBLgQKmquQIWqNjisyXAQQ9RAQ92JgUJBu+nQVMD9T3cGwYGMkAcXCc2HwqJ6LpAKau/d34kXR+6S1gOYFcjw1iHCISgpwKKRuPLENQCDzQH3jKWTaGGx9vYjPJZZVIb0hAoex9tDa1vaot1oGVUJIV4S0E8RBob0Fg5JYFi02HV/py9KVKlCABGHW6xjutZsqLampSpBCj68dYhs253pwqwfZDG6eFIIFaBMZVHFba23hORxADyNhIF9lDNcL1nNU58QKcjG5Nx1UO3EpShawAs4y6POv9XkrgL3S7uk6oXBEebB+QT1j0CJgfyZ55E1k5D/fRDjJc6ncCkw9OiNHCuz/drASa/G3wObJcpOe3G8EEY9OpebGamdasmLkQKbkoUxHXrUdRwncyEfQNZj3phtZAmix9Wywt0YpsnHi/LPdEaUxWj8xzZeoNFmwZ1Yu0AdRPUw/M2s0dTGdXmd4WZfor+lHKdZaRNpe4p4Fuf37WrOVgbhkhLN2tl5ugvaTkuYsq0Oh6ycs6qRzsvPP1gQvYggDUHqWbwGIvDjYjJPB8q7IA6tZcMiz5hLd5mYArC3qUQjxQWHqPGm0JEFp5619ikLEZRDQUBBiQc7cUIEvON5sI7UUEBUwWDFQW0yAwnq1xw0pbVhfZNh0L8aZ1Tv12pckit1zaonZnj7tTCfAEpI3O61e33m/KVR2mF6nQwW+4P3/UvvfLM9cGYAQrjSiGu7Ma056oK03A6iI9lY6NjydMGguy+A6MKmk0WGv9zpESAygLJheFajhawyjeDboyw6W6h16DZTNkzOoYP23PIejE6ERXi1AnqIab4eKiuLyyiJThuXpfkRaA+gYK7xKhrEDFjWc9Zl5Zb7+3JmR9mWf4Rp1cEiWSRmHlOp1NopzM7aFL6jMgHmdUMLBqAoM2VRjfH44AeEhYB1GjaRWUgZabBGZC5fC2oYMq+WthGsCXhX3dXPzSkgGc4ZFnRZn/jXds+79wxY+8mdVf88w5RKLaESLxxlqv3lRAeUwF5Ovah2QXcG8Rphm1DySU+KJGhlecqvRG4KmAIMbvh5Gu7heQnWEKrOWLIcyaoFzVXFgILlKOYnV+KkTIZFBQ6zvUXU6oXuV2BwuIchuhAxXajx6GrmnCIDarmQLEduaIV2jea/1TpwF4ZTrnq6PgVu7FS5WP1hKm88Lm8epNblUUVrasOBqkS+oXo82kWwQHRVBOJu4QNYyCW3saMZfzJh4w82pi2o8f9X1z6LcpKfiPeH40Q7LMGF4GVq9XQbEokyfo1okzgIZBBS09x85yvea+/+08UYbqXBaEUpEyU2+BrYAwnFRLz8XyH5SrJtI8zYWCtdixyS1bqcMDL7SpmNaj1MqUSK+OoPWjHyzw/ylncKDE2N+R42ByuzrwRByw8zHO4WnNrmASFgPEWUkLFdKIsCXZ92UiYFMmN6PmF5q8jvtuao/H4ogfPKg9xo0kkMIqk23i7pZbvfgYft4aUm4+m9nHAJVL6/CCRbql0D10KAsoClUZhwvxZhUSWuSpKgEkve8evWAEBgyjUhvX6HsFECnpN5wGRnjfUE8AyUGJFO2DieywmsApCw5Mix8POp8rldK2quH9LKC5lUbQ+asUJ8RTMi9V3axXCVDiDRqtm9AECBjY+b1bLQ8MsLKyANUXX9SyGV6oXMWzwW7jwLWOdaIROnGBO8ZVQZgPShDb3pZML5QLZgpNMPhfYQgqr6fFgZbLis+mASPHY7cFYjmnRrPS8jJow3APO/RiAywQ8SrWqEEnv13FsS7RfdGVoZcvpmQrgLyRfdg3xMQg/iMsVYbJJJ2/BWyCI8dYlQnhawxYiUXbDTtGtSkrXF0/aqDoM/FI1Gn15dBT70yRSCL5ogFVfC0QZoEGBNvfnvC6csace1eZOy+PVcm3HA01f9kUUYihFUqScNz1GWgSnSIJ8F+zsbeQ43WpFNEHwTAeVUnzhqtUiBt4+62eSng+zNQRoR1aOLJAnP21GkRsgaWZz2bQHpeCVvEeqfnlIwRYVBHY70dUWKs0XcZdL+7Pqayl9VZ8hoytghf9R4BfBuI52hdJtRJcVp8JXAksggVkEFzWIiCGAuCGSojUP7WMFL6oPVLwKhdaUWa+rVh6a6BR6TadptkrBkqNmihTPbAkyXol4JwTuC7E3Cetavt8wlS9O/KTjdcHrBlPYmzyDLCQ0eFZehm2sfqlZV9wdVhQSmEnBlSCPlFrMnLEghpso09NDKDWH0UzFOUSEAGyhCqQdZIBkAWhDvDCc1ThumKuadVG/gBDXOntmhr24Fi3qerPBvciARlGgaqenxsMKj3MdIISRDOaiz6KKo2XROHGsSMRncQm1IFSoF3LNa/aZEdOQRIvKWn23NxOR+Be302txaxUQZohDG4FMbIAxCsoy1gkd6MCoGVwecCbQPa62uL8NnpToBrNpYxaJR5HbQcoqDV8FXx1n7dN4/d68Y2++JCWkq6Z1odJYeHVmgLj4cZl6O2UO9gI++d5oxRttY0ZGonYi1rthfUw4vNMfK8VdWa8yjLvfDSriPtqMKSKmel/65Ud4cYOwZbr6Dez1HeMZYbqm3sp4EBZwY6oaYnB5VGUqiRlOexXeAVHr1JB8d6FIhHjgQJNoRTh95UULc0I1vvBS1SIdRcco0Yq7OJBn0WhfEociV9iM2FQB2jNJGJF9szSe0Ma8xJdVgHv+5H603UMbwg5WhICoAFxLJF+Wol8uePN9pIKRMuIt2MXb8iXWRhDAhG/yy7qFAYQSfLmtd5kSEAgxh09sPZ8HU7WPPEkDgi795SVpd5LLw6S6k3avp2YRHEubRCUdswiv9Cddusoj2cgd2vRZyONxVSAYD9R4TxTpu5SVSihdeelKsdaD8i3e6wPBvg9FOlpUpdIFKMUGDKDWLJ22q4zejlKbRNHwEX1izDoAu/09nbHEIuh3MxKLdCSk3Yqy7c+W31+vJOr29DufbD377PO4UeqtZXMAr2XmvCaB60r5ZTx4MaJcQIDAqBUs4ohrmrHI3ln5zVZZ/rc+cQq8oytYS0Mz3daIqtN561Q3gZoDkQBniAisVCX+NSNenAoLd2jYDiH2+OVTgV7D9g8BIRT8Duo4J4lvrMs9Un6VqXTSuZft782v3fDZ5s0YAfQMND0R5TvdwWAF6zNfzUteq6lbyaioIZqT7P45qQnsuUoHVNeSTQgbE8i/WQc4p6mEVzjEks2tLnoTCiw4pFYTrW0oUcYE339D04EqhoD6syKHnKn9tyq5GVtlhPoCRYD1prVUZ1kNYXsTb0rHNph3p1AmzOHLr0Pa573q7VnUL/KopqaBE0aw2jIRZO1lBFGG7dkGPQPerX0jsnYu1PoEY879ny2sqUFNIC/XI1gdZWUF8Lijd54u4+U2d86/wDYmkQT4Vooa9s1pyQ9hXznJSvQxGo0QJBwBtNx99SxbxlDJCrAfNbsZNFAiDaLXa0xKzETsiyY3ABaFXddvCoivIKTgVlUMp0mRjLRFiudDHsXhRcffMIPq/IVxPSzVAbLfqDGo4Fw31uKgYXqgf9QxrvC579x9blNe10Ae8+Lth9uICPK4R34CuuSeH1rR1AwN33jTh+jx4ghw8E+w+z5ug6LTUBgyxnI0Zv73MDxbsOx63XnXah0oyHh4zhPtUIxhPp5CKgm5sj9bAJKFPAcqWwwvHLAfe/XZCvMvhMCEctmK4bxOYFdhis11LzQF63VqJuiuFLB4RTQnTYkdxIERCj5iCZldRhVPgStT4n7Tpv0J6BOxhX31owfeeIfBjx8LU9Tm/bgXQWsBXWAhbNikI9DpEoFR3gCJArFBQ7lAdgvlVtQggQ1haZDncZ4ax1X2/9b7mKnHpedb0dcfrKUCNph8pUiVrapvfIwxtDoh1MzuoKVlCrYsr6GXxalfjTF+guCfF+1db1ltMAlKEXTmvNTdZuvNbiRexL1ymrTuHeip29gFhadLj7WDC+StpIswQA6kxybgQAXgWRUQvN+6jK25CUGMArt+jAyCmq6wmMLwXXvwbwUjA/I5zf1eJSXiOmFwHBu8TWw1tQy0fsefR5PkcDNAekeS0qxcpAdL/zmrXsg9WQrNehIy5kWwdWM+X1kGPUFjHc1ubWUOm3JRKWa67X6M87HRjL86lGvK4Gv2mK2B2VVakH3d4XM7z2/3BSh5zN8eydVAkE+d4dlmdUc1A16kukyMoI5MwoQijSCn1/S8B9zjYqxr7yQWL6VKMmq/uDt+fnu8EiaDV9ldgXGJSoi8U7sKa9evblgWrynlOp4S5Z3ctGpiXZQ+0998vbyKKV31BoQzebHYCzMaKcCiuomLh6lsB6pdFhY9V82nxhS4wA4Lp2W+ik/dqT9a3hYZv7yqp7XU8pI4cA/r6mEXcokENCKVHJH5cL1v9th5AM9oy6JHJVqw6mUr4hSRBqh+G+gNNgX1UOR5NyqYQWjWjCOYNfHS0/s0Nf++ZFkHVexJSqSTdnyVo384gJZvfiToBbYyHt3FyhuCwIs0s8NWiVDyZHw3rNNYKSLl8lwBbe2hIU/B7Zamh4KZXw4EZ8k/S3+QBZiYcjDrldF3XOirJfbd6DrykzVoPuG90/nfdeOiaY+H4UEIxCX6QRcQqhoJghQ50PH17DpvNjzzeaQ2IiumVQtmEZCGWXgSmjjNp9mLMf4G7UuzVJqGugPnugMRul3w8aUdV2OvZchJugLJlj3Kuk15Y5rrxfUYXXnxkVovZ5cIfTcsoFpTIr+71Lflu2fkioOoX+vnxB7qpnz2piyZ30lERuyvj1wrq1JATJ0OdXqJ2/X9BAAW+4kVI1CSM3XEhsqHcCPSAidRsbdRJdJLKMAfOzgOVasduwRoU5zED5ZognACfdpMvbO/DNaLR3rhDFkBWvj0f1jp3Vlq/ctXVoxAtsddHGUwatRRUjRIv4wlI0VDdatyc2q/gkqUxL/kive3yl78PJcmjW4r0VLwbQECpk4EleXlUxXkLH5AIq9OfMPiECRkZ65wb87PB6o1sAmIJ4iVqb44f59FJQ/mtAHkPVCHMR1Iq/dzDY+JKqkajQylxw9f6C4ZOz9n26P0LmWQkk3jdKhpqnA1D/L0Hvx+tNqOgGjadtlOGNJvV60ODzHiLJAl4Ew1HzMAsI6aB/Q4LafFN7IBG8ENUPtcr27GEPVhXt+hwMAkr7oE7XsPWatfZIWiGtEU/yjpFMbqpFiqbs8JCqg9XTvzeKI345SXN+kqmqOVBR+Fw/37x/JqzPJ6zXxmQzQ9izNGte56L9hLDqG3q9kBM1omhjSYigxKBwtN239y6q17kC011GOBVj1LV8S5iNsn02584U32mXEXcr0m5U5IIsGh71eYx3CoW2Z06bNZBHYL4NZuACwnlQZ/NVwf59U+Unsjy3nlNhIaC4jFOj6wsTEAnJmHa1cByoOWz9W8sXXaLrnaPlElEqmGxfRAinbGcRbQqy3XKp8yqbNQ77dQnufKkT0vo76xjuM67e10BhfouwrFtHWAIhrSNO0wDKhLB61IUvNN5oI6WGoWB8+Rr1a4sanM6ad526hHgCWKr8/fltxvFdP7zd49ENQFn1zfYfFsST1ruc3lFSg7JcCmoH30UhvnBaQecEMJCe7bFex41HVlW4DS4YPjqCjjPi1Q7AAXkXNP8RGGKsvmibMJxzXeS7jxlh1fcZXyTEu0WLee9n0HzRVayMgClcuPdGIuCzYPBDKCt7D4CppQe0xm0Km6a3BqSdwV3noppsnSdfD4oaXQIhKQNq/6Ffih4ifQQHKN1VnwswvkgY7hb12p09ljLo4QQ5n4GcUc4zxBhjFE1TUApoiOrVei6QqEoKFe+mwerRDw/AeNc2NkJoDEdjiEldT+ZdJgHnbJqMgPCA+S07yKRPpttmFJ0Lp/Oqp94Mss9buooKYXWR6HoVrBbFi2Tt8hMwvkoKwRhzUZVKBmiLjBYR8KpFpuHB5vMSgh4eHwW0pMqa9L8thxF5H1tECD1U778acX5HqdG7jwTTXdkm4LOWV9gUVrivRNWs470TDGBOiWrvkQDpELFeNQgsXrR4iLNg950F4eW5kpLAmmddb4e6FksAUtRSgulqwWE345OrPfI+aFPEa8J61fb99MLWjkVnepagGsDj95A2+APsrCccvhUxPIwYXA9wUdp1JXKVFjEKM8reOiozkHeh5WC7es9ieaIaJV6yOZPUMhctQk9NjSXrgReOC8JZz8CytxY0HjF1hIoq+xVa1O9MaEmq5uFapz6mj2dMH8+qyfnVHY5rKxoHzMn7xPLfEUgHoIyi9YRfYLzZRgpA1dvqB0EpwKbdt6EHw2xFZTtJhc/K1LpaOuYPsZoSapsNQBVs1IisuxaD/yqdV5oXXz/v8nKdjbiq2CVnQTEjIo5YdQebe+EArMtqsQjQih9Nybsmwx2K8A9m/Sd1n+86Wq7bpr9QOrfUi0CtRE92kHLWaNAQLFSM3A2PVbJ7kzgX6c27oMWi3DYfgMpo41UQTkkFeL0FfNK6KDnPkEV7R0kuUPVzu6nS8H19JtsmkHbDjcXIfmh2EclrxmYd+Z/Y6whdxNXnunq4iNrvnWpff+ejg/N0Lo0gcel1dmiAFigXu5aiZJHSYMlal1eVvgtqOxi/117v8HKueujRRvEch39vkF4ezEfwmrMeIu6ip15OyT11oB2U6sT1oZLjax0U55frz2DNSqIxqBmcEQCUWSdPPN/qAW4hlOI5HVQ4tgyae+wV1kFN/1D3AyzyEuR9h/9CVRnq/FxC5P6Mi/2uh9idIRrsPi8fw8X3l4M2c9yec5VxKqR7wdqpOMHMBREI2i9LuxxoxwiyC3eSmXTrePO5lpvkFIx05u+BjaECARgJdKGQ8nnjjTZSlArYeZxONSfUQ7nXynPc1KvUtditHeJlsCZ75rEIC8KRMdyZbM2qizjvualaSPsiEUssOvMpaR1PYPCawetjKZCq7MCEsh+UvbwfWk2LKOSisiXSetL4axkKH1gkQ96ELWfrP7UqiWActaB3jNWLqu0dnLLvieLIAGmioAxGU/cNmwWhCOTU7t/bOQDoNmbb1Lwo9Om1Uh49qOEySarJ2y9YLqJnQjo1d14gq+ZrJKV24DKhqp47Bb1i/GqwRESFPouxliJVHJ7IoTCNTvm0guZFIzFg28W323R9rkAs11VG7ZXjOSgSaO3ITucq7QkzrFD7XhQ+trbfvGRkJqSdCo3WNW4sPsoAi9WtzLqGp1e5Mey4sTV5KRiAhhzYgSRDY5jxWtpBZmocKhlkxdHjoOslssJ+qQCsRBit/enWcSBjI1p5wezRhVRHBlBoUhZ9xo5Q+Ojbt4uYCshhqGxSRxHSntVBsnwcp9YRG53EUSub0LWZwaBIYAj2HwqO/+4ax0FweOjapBQokaenUhvs6FFqnoycsTOEYbQ89Az9fwaq+PLQdDjXa00nuHSatxKqDqftiXgWU6q/KBuwPF48SnWsHaZrrYMM1bGCbKXkM1yKyYuiy2D/BoAuj91URFCdyVo7aGtJTFrL97IASm+FIkXhnHH4UF+z7snqt6CF3e6g9V9fYLzZRqoIIAV9+Ey1Jqd5v2o09DVe91ThEWpJ/bKz4rNBgFhQygBOhPggluSlmqOqkVWxzVbQpGGKHqpYVlBg0Jw3qscVhuy8u7IbrF4pVMooFakLjhhgK04tu4iyC7WCPCylGcZFmVp+qBMRMA4ohxFltALaoYnXUrbE6Nwwche7dUpvP29CQDiRifei4t+K94eaw4JBOuFcEO9mfX+GYtpEqtAwZ80r7iPoEJSWD+i9u6dvxkjWVWWegJroB6D31/f0KR5Z2cHdGRPOxYxUF/WQMfuOqx74pxlyOoNG751Djz3C14wSCXknyLt2qADqbXtUkHeCMirstXOPVsyTXTNoUqhvve7WCayduclkjQ+C3UdrIz6YHp2AgUHhHGd12QTV55onRjlopKWQqjZ65CMAGA08WxeBUQvDy8AQh0GhjM+0vyDfQNms4/3jOWryRWQkE/1sVWcXZb3tW47CjXselUqujkVBfMhmHPTgo0KmF2j0b6ZOwqshB7QWsPuxUaOgq/czrn9Vf7bcaM2UmKgvuWSWT1+2iP6UgEDg/YAyMoIZHAxao8mZrb8XqiEoY0DaBUgkPLwX8PC9MOV37d/kJQ/Dg86FK39QploSUtMTRfOy4122MgjanHGVcThn8LwCKWtvOTPYZQxqMEmdz1YXSJvPAVB1GHlVxqcbJiE7G6JH6FLXVqXlnzPGl4s6N+9MmG/1s9wfqbmzTLWZ5+eNN9pI1dF53rVkTNzjaFGJ/tz+3xX4Va+w6KEnRVSDzQyQF/JVWih179cPy1vUqe+ui7K03xvWRh3T0PXSGixH7bXGbgIUuqJcNInZHwZZLHGuHjVZy3Q3CjX5L4Ke4Qgmw61t/oyd5Yt/qyZuzKviniCqp+vsRmWDU2UrNUhKoU8hfUKSAbL7Eb8fdDCH26uuqFKf0Rd0vwA8KirtvNYNnOKRpBckm3GsHq5fji8t3h7SW0hZ16A/xvpXhArnwSIsiQ6VNS+3H5v1Wwkmgg2TD6jPvEJsrpLfTyTYoi2NNAqRyiNR0WLrjHa4d8l6f2ldC5dwINrB4w0++8S8MzF7irx7/TUv18NrDgPCI3NBEe0bV2uUOqdheyGffuhVfUJW4xbOBjtPavCEUcscNpHUp4x6P5lqBNVLAjnDskK4QTXsSkTd32RSWKrrac/ZC2Oli7AMtqvXlkTzrRWr1husGnyOQHik04+u2Flsv0lQZ64qXFyOAoX+GE0hpF//tXYPFf5jmNOegQIBM6EQzClWxh99wa38fw8jZSfCBscWnSRiQRGGDDCPwtQVWFlQeaBaazF97Aw8V1LX+op4Kq2Y0tpvKxMMlcUkgZBzAB0GUA7gLvGNUlRpYMN40VOgDFzl8nWBFnBitTdrUZ28NYG8yA/YNITr50CGgPT2lRInphG0rJUdRGtBKA22UG02bl6vGRxtBNkOTPegyYVKoWeWGHW1KXuQKovH7UHBc1YW3ryafBOZ8SrVODIB0fNYpD/QPENnNDhodFMEsiyai/q0ka1+h8xYWo1YJUC4w+2bvqBRq8Wi86wHWTxaGYKpT4O1FkXiduvkCaBicA/BirUJNLfaqnp/AqS9fq8R/oDpKlRveHwlnWe7PTWEtJEi5dDkvByWsakPZ9kkzklEPek4IB3Y2I1WP/QgOLwPxDvTFAyal5Ah6DrqyDAg1bobjtjuNaDugRK0aFup3y7JpIy86YWp+gO1M0AZmjJ8mIHh1Nboct3IK374rweqlHEQgcShZtPmK4CLLQOsdV4mT7Rcq1c/kEb4ToAJc8uDxpNHJG5wGel6UHUYwFjCuj53HzJ4GeCixV47JYGUBNFFIJxEEYjB2nAcBE1ZhGq9mxdKh4Ut541ahN3nTGsuqKYMgFa20MGtlgP3yEejaF1WZWSkfcC6J4RV0RKVOtII10t4eMmqHzqFFoX1z96IHUo4UUkvAMpYNuc+WZqk2PlZRkK+4HV92vi/h5ECHnlR6lUUTdyT1gNUgoRtjPlWiztBACVgfGEGyNoo86qFtmEp2qjNigSDCTt6IbDnkPKoHFHOVp+1mg5YlmZUHH6JDOHBlIkt75KMCZS9f5SA1qSFlkSK/YqA5gQ6z9tIIQakL99ifTbqIh8Dwtza1KtUCpQWD0B2EeuzqQlP5m5ju8fdsX8oX/awMoPpJI3iBsH7D3meTHNzWN1I25LLooluQBPVXfsCBI3uamsFEa1ZwwiUDKSkX582TC5JdeiaAK3OP2po1KITVMi4zmnOyro8qvebmJDNECjWvuXP5pEqa83rsABRWOpsh6zXaLFCf+lK2yeAVY2CF+1SO91Lq6Gx1+XRSSaE5VoPPlU1oU0E4m0zVBHf5tDII54nKCMwP1MqdnqljUDDkTVvMHT5sKQtzyXqYe+HLee8uXdhheayNfpcrwnLTbtPicBwR9h9Iogn70xseTxz/Lz1x3CnNYHLbcB6w5tcYI2kGEqNryGuOQCBQSSbfEcJmhvKO0I6qKNZm4GKQY+zVJSklmVY/ZtT453p5oKznIHpY8Fw36I0EKy1iio/1HIB8nMFoExIN4J8pfnAvDDSqjeoRi5pi5xFmbX6PM2BNcKD3jBqvlGXtf2/d95s3RO4Gih3LDkV5Bwhb1urFyJMLwXxlMyoKw0+nLWcxUWHxRyLysrt0Zyi3Qo8ZxWOK8I5mTOmogglah1oGQS5b+XzGePNNlIOoRiDrE+YvnY41AX7O19c9fdoIWuFHFouqhYJOn5eLopfLyCk7edu4RkAm4UDAN4JVL0fv6cOWird+/mwwlUZTPrJIUVTX6YiYBEQSvO0jEywGUxAoWqglMoslf3TO/QkaMxBO0zBYvBlM1CXg4rCgUrjLm1e/FosxyZeyAmgb7pIQXt1kUcxpbw+ourro4hUBJfU8LG2vqoJXF7V4aisQAAgM8BrQZwLctE8CHNjzT1ijV5oNvbMvtqoDmj5S/slrxpNVVZj0s/Vv21QHXmxp79/PbhVeKZnDTqBxWGn2pLECmolUkOKPAoLzbnYPGfzxJFFHwOjQep2fwBqwXsZvJ6rGeW6b7q96YdcNVTGrtu8tS07N87973oW7qeO/hnZvD0qjhVHPAgbiLdGLLK53541qWjK9tq980H/fJx0wMnOikocUKjQ12AlMfl+s2stAwHR9mMM8LIOV+Ro83VxxrAqdNRyHM+BmkNaWaB2z56f7wln+mxV/9LlzTx1cJmrJZOu0vYoPgdm5F3Q2mFgIlxG45823mgj5QrX7ln17L5LcVTHy4ntAQPglUynrRmg4oe8OYurS494224G4gAA3KAdslB/yVaUm0HnpAw7AIKonizpYqt09DWDbTMoiy7CIZqQrPhuVIVzKkZRd/hlGgEi5Gd7pJsR3uDMi5a1liVqQvZlUIHbIsCSTCeNmoTNQG0hjqEqaw93BfHBtPG6fEWtWeoHwyBVPeC90p0Lm0SR3bPVblHXrFEwVa03XrwIGpAhIL99jb62BFlA8wJeE7AmyMMD5Dw3w0QEGgfQOCodm1mNGhHi3Yzn/2lorbgtap0+nlv7+MCgSQuphg/vEV8Gbdb43g7L1TZ6UuFTvdc8ASXYGiuoxeV5FJQvAVqrpE0deQUO7wsO30ntYLQDw2tcWhtyQhkFEoL7AkpapBbl+r14XiBPWtBMSRAeBFgEMgU8vBtw/9ssSf8JYXyhKuiV1ZnVw64adPaMg3nRnoBHB2MJA2VinL7EOL9juU+LjMqgLcdlKkg5qLqEGW3vz7VeE+a39G+1gD3oniRjAzKw3LRCaScnEHzvkRFKpRrVR4MsSnvQ94tnqVGZq8P00ZpHxFU7MOsq1cVqn7MA4WRQY6TaQqQK/9o+r8SEosw8XuyDSM+P3XeA6/eztkx5uVYGr7IW1bGY3yZtONiN4V6w/0ijfSc/QaBn3xR1/+wH5EOEa5Q6EkKr1h0ykRZ4T0F71k0ECdHksrSspYyM81cm7RbtBlxgDrO0vR4UTEl7OwiIqno9gGbcgimPDIS8/BYwUjURDKB3qcQO075hn3uinvQvcPoq6eEyUPV4yUJ/r1nxUH5DtCi0kQOpSeFFG/LR0kFc9kVstE9vbWENCmVglEkPz16zjfzwiACdRZlXxbqJWuS0vDXh9E6sG5itGd960KLFMKOKdVIuCF0NVBWTLTZXpB16XfZ/AECnFZQzZBpUacBrwc6WaGHW0KR0DoPDMp7QD0GfTrJaFocNc67Gxb20cBbtSwUgX43Ih63Wk0oXDWoo51Vhv1X7RVW18xhVqDMGIIrSyUVAD2fs//cFLp0k3hrdGzfavGAc1GB9+AI0z4jPbjHuvgKhAb0KiZCJ0A6aAHeCgXq5uh7TQZAO6vEOLxjxgTDcC579xyPC/+c/AszgZ7eQ/dQ+HwBiQJkGI7GoTIYrX8P7SzmhAagGA6Ieb/L1fVYiT4mM+W1AvnbC8mrE9FHE9LJUjbYysOafDCalLDqvImjivfpMs0GCqnfJyCNjeUY4f9lylovSuksEZJfBVwlp5jpvSiKA6fkBy9sZMhZwihhf6j70FhHCwHJLrfuwAKHrr9Ublk0NGLq/gRq2wYxKbWPCGj3ximZcu35U3reLZBux+DpUybKCsovgK2VCgi0ytPxcMYNNRUtZhJvqAyVg/3HB4Vun2teKkvVFM2p92puKwzPZRO7Tx4TxnnVve6RreXnVUQTyLmI96NkQTxnkXXi9RCWoUHY8mzM0aDQcz4L4oM7iPE04P2ekPSGetO1QJZZY5F5sv6uWIgAKKEGZk+uVweBOoohailEGIH9B6/NGG6nPCxkrO6o6m5abMm/UE+csgFi9yqPRz5BvCocyZPs7D5WpMqR8h/Q4hWDDUHPYJWnzRlcb34w+aOkhsqL9nYJtsrBqzZT2dZHa/rmG+UUjEfThfqauZqqDACoflSAIj+a50k/JICJ3FowB2BEUL+69u/8eyjMoVimvhp0xtdqwypS0jRrU6BNpLQgFBqZJBW/HQem3gbXuJ612XebA+GFMRR2d/YhyvVcYxfJnCoNYbsvyO+4A9Ioh5QSURFgNBi4BlYkGn0aBOjWrkijCjNqTiwJrqYAZgdrOvLKzqEGnF/PZM+GImrgxVVRBWgRcn0Hn2YZG/a7svR5SvvzyuSvtWuprBaDc0fUZlSHoCtiq36fEDa9Z0zYQVKHzPOp+jdB12ODnbg5sr+VRSRphEZQzgxd7OIXqxPjcaY7aDbmuLTHH1X+vEHv3zMTXZRex1rn3/aLRZyV3+BrTh9JBW53BzKglBd45+ZECiJ8zHaQGQWXg1df2RAmfH9+LF06MO43VsbQIS2HIpnHqTRFJguXj2vU4NNvPba8U4tJNl/mqOixKZQLkM9LK/XijjdSGiloPH/teRKXi/feeGHVsXbpmXvoCXd8d80nFL83ri4AEY0Blamwv//ii6uX5oPVOoZRKDOhFWLUyXj1/byFCqWhDxaRafTIN+t6+eHN3sDvJIWtyZfg41GJaV5uQyODno27gVQtVPfnJxzOwrGAA4aCwVjhlhPvFaOJ+MOgizFfjZpPS5Ybtja4n2g2CEU/Yep4qF1WKyFkhuGms88Jz1jqxKSBdK4svPiTEu/li86oRczYXkbYiof0e5StvIe8HSOcghLsz6O5BDcFu1wo+TcVCxgHH//kt3H1NO40++98XTN/8pH2eMcV4zeAlVG00V9fPRh9/9f0Rp3cF5VAgJ67OARWAz9oJeXqhNTrDQwG/OiHnDEgB7h+AeQZiBF0d1MC6Mc/twCkXzEmnUm9kbYAGFZrYqB9IvAL5IYIWXdPLjZI6wsyNQg77f2792ATQ9xCx1hLJ2KEqOVSiGt/hJVnHWlHChCEOcg4gEpy/pEZleLA2JLMAxFivlQ0HAMtzKNnkRApFQqOtPNn+XEiBiBE4HagWEuseFPPYveTBSUnNwNY8SzA1FbdnhOqcaR2PFfsXbAlDvgyTF82mFpHDjQFqnioP7ZDXFxoz+KU2U4wPRVMDNQoi1b6c9MubOnICeCaMr7xeqmB8oerxuhf8mXMV7fXOD4AbGHWC82EAWQ8zJaskbd46BWtPY5TyEjXFYWUTtege7hCbT1CJQtgYZgA10nNB4eDtPgDk5XVRwePxhhsp8w7cQwFtFlNN/BJqHYizcwAvgnUlAqn9odZrZfLlYsaIoZ0m3cswA9Y7qLXYd9QFK+dQaeYw/F5x/qyHYwzAXg0apQI6zqDTDOwnjSSCPXWX/u+9JZM8okzgO9HiPaBGSBiiseWiblovCF6SFvmez6AY9LAJpJT28wKsCYGtSt6KovNOW8DTWmr1/qPhLKJSIAkmvUIALsgRpVR4TkIABZsjYxxBgHQYsN4Eg0cy6HhhpExXT2KArNzmaRqRnu+wXsX6PCACPgelrJ/O+rf7qRn6NQFDxOlLAa/+J41wdi8ipv+2NQZi2oG1AaZ1PgWAwdbg+e23VH3AFN7l3NqccFIIbLgX7D5KCKcEOp41UksCKQKaCTRNakjrc4ZGe2iOQ7so2/SzFd92KiveDRbkeSNzBBJAi7amd6hNnTCCLGgEnf55ibTaQda150r/QIM+eRUMR4Nx9kCZzOAVWwsErLcF6Uqv6+rXBOPLhDQNCCf1IFVsVl8XO1mhPDXkwtlyJQDrrSDtgeGBMH3CGF4xmKTVffk8WY0i0Hn3Zkj8b/o5rYYtd/nCC4IOeZlDztW5q4+mg788P7d5bgkYZi9qttcbKUKIa67Ym6iSRalhBnafKOM0Hgvi/aLpgikgR809IQA1hO0D6KDohjCBAoEMsqVUFD51UYEIIKK2kO+JYSWinoflTIjk0W4zZE7A8OH1lK02TuFVzoK0fsp5cjHebCP1WcMMFByvJoLoDFYMVxOdqAtsE5IXWFddZfAUM3Le3gFoGwbQnzWxVLJ2BfzIA9uw2frfeV+my7YXlmdo7DtUb7C2ALmEPJ0UYXTdTx198ajXUBVTkOhjdYcySCdTldnjo/eqDRVhESPM+BiNvm+sR3RBbCkCoGygtFqc6lFcMUUQ924BYIigwx4Sgyl+FFQ9NzEKdoxKphiGCqcpXVk/O86C8RVXqR4xtQmE0HJd/pm19q072ZgQZkF8xUhlQDwy4lFfU0ZARgHIPfTSaPvdEI84UwKlCAl2aMEPD9pGUuIQi9R1oB775VpAfSbahVcp2Moo8zlVB4sSab8xM0xIrY2Hk1aqU9BfO5nz5ohDHzk4bGZFr73qvcv58OrRHyDRn3Vb52Tdiasw7eb++n5wDBE12P5sKQvYQRZbT04W2lwjGnxWDVXH8NM/aOkFEjSExNv8OLJja1fbCOm9OazmB7YTI1qtte3ZrqjbzyCeNZANswpa89JqFmv7HZuDR+QRQbteR0EIjbFoijK14Dpv76F/H3e4/Nrc0JMA5ILKF0W6Nbq3dagao1Ym8VvCSL2OyQNs6nQA30Ss+zcVq5kigJvovBC0RsRgQc5q7ePJYIZINXRfrwjLMz2A6oMqQNp5R9QAngPYEvbwKKr3ULvNL5Ehe9PXC21zATBoC5rsdNiB7PXGbiyjRQ9ECj8EjejSTov06i7t80F1suz/7hWeVwS+KNgzEoq2FwkoTKCx0cAvNbhozeCHE2Bdc6UU9cTFIsIQqgGAUWqxJM0nFe9cCjX0Ho06RdyiGh/5+TXo2RUAgI8rxuPaxEaLQVa7CdhNkN2EYhAnA9VoXv3qjLAYo+8hIz3fIcwDwrLqPRijsRJanGbszzID+w9mvP3vd1Ud3g//+W3C/KW2RviUQKcV4hp5AFAUl5Y1qbq7iBrUIQJQOHE9aFfaOgQIi0Xhq7PMYnNKqpPWcnzDnRagUtZ/h1kPmDwS8hAQRz0gwzIgHBOiiDoYuYOuA0NKp91nt5D2mtwvQaE5CeZMJLZiV6faE7gj84S5YHpJyGeH2PXZ89xgIT20HT6Cvq85bxW+G5RZFhjgOVQD43WBZQyQvTooaSKV64nYGM3pVcH4kFUuyltduHFzNqOxUCspK0WVKRu5tgRy3b0yAuuNEiiGe6ptVbxZpaM3FRGaBpVDilwp7h6B5HstdB5fJtXC7Eo0Nu03ikLuTTzW9mRqWqU9FVyLjpvzO5ykdguoNWrQM5VngB/0mXtpAQCEE+o52TuY3nyzqn2QPr/xZcLwckbKZ3yR8WYbqc8YPduHAlk2234pUKtuEiQbD8hoqSLWv+isquRN5oUAClieq8xJb6Sc1aO5K6uAdzqvJ+s3sJ1dDpF25Iwt71QPMI9ORPTA9mjExVRVrqH9LJvhMm/R1blfJ+bYe4VKqjCa+8yV/FH72dBgtSAE4aDClCKgzBUKcS+OFgFOZ8jxtKnHUp29gKrK7owx0QOrkhIYqH2Y+kjR588Mg4SAstdOpjxnhLu5MvXk4aSH/26nzLkQILtBjT4ZIcNEf+PLE67OyZQCIvJBlTF4iDXaJG8X4qoU/fWIIN7NuPqWbXhXUoiEdBWM0WmRw2LMqnIREohAcoasyejVBGdBqhYcVTqvr+HaoDPnWoT7SAnCo1FoxDjcq1cfT7DyCzVSanQInPSwpSKQBwZl1sjKvd7N+pVaZ1UG1DyUDBZ6k7UcyQ6pK6NN1RkEriEYzvpeHIGczUitUoVfeQGiHXzVS5cWHQAeyZHW6nmdn6AWhFMgJQJAD8908C4GsOJcgdxD27VkZ8GV7fojVFo5kxo+qtEPLkgolpNyqPKEqkzhkURVsLfnVqJpd7qjnLSucCiCaHJO8SFpV19Y9AnU+/VzbBNN1VyyVIUXV5VXZ9Y6GojPhf5N9rYwaHPMyYxRUackm0NBRRCtXVFvmJT5R9XYeVTl3Q04z/gi4803Us468uGhLBlkwdI2PABnTdSQ3qRFXIUZsMMETWNvAwOKejjDPYGXRqUlUSJGOKlCBQCUvao4S7IuplmAWCr7yj+rjh4e9P93Xrs4m80YZ+4ll1GFI2lg4DBqUnvqjS+ZigMpPbsUhb3cowoGf3nUsqYapVEMWiy9auuDR4wdbl5lvZMsCMMADFablTNETI08Z80XjAb/XbIGszTJKZf2kf7ZQfN6IjXXIIk7xYx2XUa1hDMtveupkD0bq/1pUWKXNE4KadIwqJJ8NFjHJSAsWe4RUS2krgdGlyu1A0D7bAVwiiBuLL62lovOFQAMDudY08hVqrySH9C1CNnWkctTKWys+0AiowyhCeXa69nYipUWbge2q1bzwoghgKLNf7SkUAche5sYITbGoh9Q2hjP8736LIH4AMSzCqpSdUK6gx2ocB4vRmwyVKOsqLBk1bvz+yGprDIKRpEvEZSdeefszGKLNNT8CidzjETfO++DFliLrT8DxsTXei1WVVTAO3g3NmAzpN7YswS9n7B6JCWNmQfA9TJrh+xA7YtQHeQGT0pdY3XfFKm/90ajxSF/YAOBViX0Lt8kdvw55Kf0/4u92TklvoZ8z3p5RDAyhBChJKjDzlTVejhD99o0tm7PnzPeaCPlYpseZmpY3sgKSiSQFikAICkGZQl41kgKgSC7oGwY3/yNt64V+kBdiOOrguFBse/1ijA/Y4CA3ccFu+9ozUO6mbA819qXeEoKFXkC1+mfpQAzKgvLyRVNL8+grVxUUWKyKCByZanlmx2W28EKSi0JTu7Vebt0yxcNAdiNoBhQdpOpnKu2WRiiecwmuSQCTCNkGnVxL/KIhosYUK53tR2B99GJgcHzQed2WSEPR4UTS0Exz5GHCJS9RX5qSEi0Ydv0ggAxqMYYZjohFnXNC7xteTh1BBWgeY5WwIvQcn0ysMEphHVQVXCgQU8aEVBVNyj7AeGw17kfoxayigBDaJCyRTt5P1TopNKHXUonQfOaUZCuR4TAGF6MUMyquz8RlGUFcarFyGLSOON9UQkkW4ck2hHVhYVpVTkhBEKZQlWvz+LtULgeeiya2xiOBWnPmAdC3lNN8nvkM3gH3g52lSHYswL4nDEuBWEKmJ9xrX+hAuTMxurSv41HwvWvFuw+scgme00em46fvs4Zt8NDwfhK768a/kh4eDdgvTJjyH4gokJUGo1H5MXar8xJI/slIYiAI4NK1MZ7k8KKUTTKXw8AFS1m3X0koDuzmKJGRIKy7bSwljGkUjvwkklOVG08Uu1PjyDGe+0g7j3VnCGKIkaUMl1R08bLE20aH+p9NkRBw81mhLQTAbXuxm7crCsBmboFSCW2ykRtz3qe29jRcS4ILy9QH+jr1itWHy0LcG4Ruu6hgrAWsEd6znQcGMvtgLTnqnyPt/ZI6dLjff14o43Uo1FDS6oRFjGaMrAPR9WgxkuEt/UUlSiAemD1NGwVYsyGZw/V+4ynAj4uerDeTLVKnEpQ2I8FXMIGeqOiUv+I0EVXbE0Wu24rqHSB2ctcUbEFVwJpAa+Rw1yV2avyEWy3RIOfHFtnwPvOkLdZX1eF/kJQj56oNR3s53GIwNWEErhSXBVCsI03RF3oTHA9PXHatbeEYNZ7taS/UpzzBqrZwKTGkJSUKnxIboxC2EKEFg1WIolRdUvQeh1XkIhnhSsAZ6uZMvYQNC8UQ5fQNm/U144rDAxcNfsqxFYjc6owZhm1rYOqYRDkkghQskpHmUCuaihasjyj1u1A0FrOiKiqtGSzjmgedJAGQ3f5Ku3pJKDJnBk/CYoWlKuYMtc6ubpvuto27p6P5vD0PssAg9dQiz45affj6aOzaenFVoNjBp3NI2eLpr3VfZ2aKYC/FFoE0PsmDreJsuKEAMqs7EuDugi5IhMSVQ2EElV4rUQtI+HVIg+7NxVTNrkyO9jZuz4buaSPoGDGM6yEYq3uw6z5p1rCkBRRIUGD6yxKddX0DZsTdem1e6Y2Bx5JafTcpyZ8sVI9C6oSRh/FdlG/Cu2mamz0swi4MYFij6SsXY9HUVQAnrOegT15YgoIUzBau+bgM4DcO5efMd5oI1XhAj8U0HkbzkQDAQEgOGaPxtKx1wkb+8Y10wTw9hMF6IwU4AQEX1Aqh9T6UuXbXfX+eNaHGI4J4aG1uBf3xB0GjAJka2OR28FT78NHUdJHb3TJ4AP1gAwms4XGWQ83Xjrj0mHstRtt6vJmzKBpUhjJa4qcDXdxTRJ1F/Fa6lyA2uEJUuiJ9ntgGHUfmGo17ffKootKl64FvCIq4eTXM6pIrjhD0D5/EymZMZLdqEbPFSSc2GDwoNc6kcnXxGO/k2D3os+ytpK3a6I16wpyyOMCji27oIr6o1fu62GTrgCJqn+33BIe3ouIp4BwfI5gPb/k4Ygyz/WzAECy1rQFEdDVhHQVVcGAuR1eHAHeVQV3XnMXkZa6bn2fcBLEs0FP1ocpnAumV4Qwa7+naGKmYRUVjZ24QuMAjBnHlh+yiGhQzz2cUfMSngPm1YRT7faKG3sPCiwnVkYo7fngUSdBeNoaqagyVD7I6iA564GeJyCPRl4SwhhVPJeWdhiSCIZjwfRxQJyoseWKMedWvffX1Ubp69s6z7sI0F7v3/Jrte066bPk3AyD9scSxBMhHNULrpG7v38ScAEiFZSLfkuUBWUK1agVgwir5JH9TZg9z9VeW9UzYKxXKwsIq2jZSEFtIEnZHJchVAdCqAnL6ge1baPRY6n96dzplDEq1Gz3F5ZS3wdDQH5NI9jXjTfbSAW2Cmr7vktwqp6fkyAufuf5AsdsBeBUamPE/v2o21D1cwlto2aFYjTUBs5f3kGhqozhPmmh7ssT6O6o8NizKy2mSwVhXkHnpRoDltDqotxQ9aSB/tDMWv+lje+cKWULtQiCQx0CuCJ2bzgglsS3A9ijJJdcekRlZoa48reL2pIaTGdQBbu22vaCGbJTiFGfV1Bj1DtQ1BqyAcDwyRn80Sv9+9sr5KtRje6rAiR7QL3x9MhpHJCvRsgQEB4YdDxrn6qUQbQCzGBmRHv24WEBPZz0859dIV3rNVbyRxGtKwNU0+60Aue08Z5bQp1At6N2jd1rkezyXCok5c0cT+8C5y8D4cQQvsItfQV8XkEffAz6+AUgxSJNgcwzyocfg2IEf/lt4Es7hbQmwnrQQ5kXRrwNFR4b7pPBxYLh1QoE7SDrrWm00aB5ykct8OaFEI/a3I6XjHBaQVmwvrXD8d2xFm+6Ea8SUGjRuh/c08uiBkqo1lKFsyXcrTDXoWGHHsugunx5ZzqHk+6v4Z5wvgtbyjkZEcGJBQt1bSeA1aKPPKpjsPuQsftk0A4EBbXAd/x4wVurbPT1nABQIwTPh9b1j5qD4lUP7fXZgFUG1Hq1NSEPjBJDg+ESIKSR6XrQn00vCZMpy+Q9I+3YnmFGfNB8cJm5pRlslEBYbmMlGPkzCUupWnverLCHogGgjLEyE/W5qTHsKfe8WpG656wmZylSy9e6EbbnUVXhz1nzf0tSdIgZ6XrEeh1rTjUes0po3WrRcF5+kyKpn/mZn8E/+Af/AL/yK7+C/X6PP/SH/hD+6l/9q/gdv+N31L85n8/4C3/hL+Dv/t2/i3me8WM/9mP4m3/zb+Ldd9/99X2YJRk3o/+WSGujqJOWQYu8vL6AIIB7AaTGDf7zy/cEACaUTvbDc1h5ZOS9wgu8EthbZCwr5Dy3bq+eaC164GnjP4EqidvC6tl4fhhaTquqJQP1UCKCGTc1XOG0Vg088ZotoBUVA/a3bTF7KI7BRCmtaBhAo4ITtUSvfb44i7LSxIGWZPeGfmyV9JbT8ER/Ec0VjVwNhArGEiAHhdyKNk2rRtYiux7WU5gxGNOPuznLjZCSclXFoOMZ8vEL9URjAI+heqWtrXq3aDpWJa2WK+uhxYJKoc6TavZJNI/VPO8yFYXfRlVZSDeWnxpHlUfKgFehSs7A6aSkhGc3+jNGNVTVADJZuQQZJV1baXAqkEwg6/AKUoMSzON371t1GK2lzJK0qDxl8JUaqPWwXfpasG5LoWPqxSO03MHXvnvntcW7LbkLp88LSJ3IkK8KhAUkoc3dp4xNPQ6hNpLMB9H8394MEFvuzxTo+Zww2PPLu6jFzw55UYOv2pu3f3t+W2WZTG9yVoYiraWSj5qeoL6nR3o+h35uZdPKZAcJsp4lDFQ5qXoZk+bvtDMxjJ5u8mfJ8tlFVAmjR2oMacjOAhYxJ2KLpJAZKe/w7IzJ3MPFfkkdgqU6h0UFibu50m7VCp3zqp2fKVi/rZGQHx2srx//3Y3UP/tn/ww/8RM/gR/6oR9CSgl/6S/9JfzxP/7H8e///b/H1ZXWs/z5P//n8U/+yT/B3//7fx/Pnj3D17/+dfyv/+v/in/5L//lr+uzLhf85nduZByTdhjFD5YOixUY86WGst2/6xtu/+25FoUrFO4opt1GJj7LJ6vZWZOygbKqPvAcKqy3acXhw1iJUkjzOiJVQUANbwFKB9kZTMlrrgu09nACNNd0MQ/e4VfoglEozQDqm9L2umCGSbY/+9Th7yeiQpTVm5P60uK6dQTIEMBXezVWAPi8Wn7ON5sZqL6olNtcVG8kWpPETqdMo0c7LUtpdHjoyzy/BGbUuqyL3kmPYKANfOqeqer0Seft1k0dtYX8esWYn0fEicHLM4QhKrPv4QiZF12PFtHKOGjbkDMB0GdW2507FOewXoRq1FmS3HNP9SbtOgH9ewJMvaRzjJygkqUK5fo9eMEuAERvMSH68yUqmrDeqJoFiTZ3pEIY7lUZokoYZV1j8SyYXqgC93pFADEkiMk1QXklqq+LRrO7XGOojQuFgPige2p6YXBktATWFOvfN5kvqfmcCvMXdfRa4XZbW722pTpVndPXrQMm6SIP/UyP+lSY2IwZ18dSI0yQ55XMmUu2h6yOrf7c1M8rU9DWgAzmBAeyNILS22t+3VMdRaF6Xoo6C0Z/l6j9t4SpQtgq+mu9rcwwltii6JobDQESQi2LcSajtrVPgHVm4ES/edp9v/ALv7D5/u/8nb+Dr3zlK/jlX/5l/JE/8kfw8uVL/O2//bfx8z//8/hjf+yPAQB+7ud+Dr/rd/0u/Kt/9a/wB//gH/zCnyWvO+Br8CPN0Fwmp41cselvEy7+/Zqzd+tZ6//KwLWBmycmOWtylF8dVQJoVjKFIIFOM0JvMNyA9BBjheWo1UIAXXRAcLkcFG1KBhHw3VkhLDtcq3KAv7ZLeosIaBdb9NAbKo8SNje//V7hxtcY8zqJgio8W6CrmY3oEVvdDOBkBSj8sYtanGv6a3xnxa25VJgRQQkNNer0x5qKdRCQmu/ajFKAeamsKowDagsVbwzIagg191daHuzyvrp5qWraWazLqkYQknV+BLCov4CmDCFgeRZwnFUUNe2vMbyzRzxnxI9O4PujRqAOq+4G8JwRBQizFoU6E1HXnc6nzyslRmA3SC1HcXk4y8DAqnWA1et2GFe8jmprpNIetXcSPaiyN8jaaVypMVmeCfKh2D3rtQ6fBOw+Jowv7WAzSHi8y3btwPkt9RwlqjGL93qdXtPUKNjbR8JZW1cMR7FanmJ5JmXSyWjdeSd1JnkuiEcjZWSLJAAt4k36fL1ZpLNwJdk8JwGFogLH2axmvxzMeKgOKNW6IWcuahJHNQzrvXh0FlBh79rTLRXERVm+xaLiMug9e16RO8hOAqHsY0UzKgEsuoFD/aKkRIfwsKBMEevtoOw9bvOdBysiZ2B8pYzSsBTkHWM96D1UfVERyC7qdZLDvZqOCMcEPi76s3lAHuW7h4L+8uVLAMDbb78NAPjlX/5lrOuKH/3RH61/8zt/5+/E93//9+OXfumXXmuk5nnGPLfCr1evXm1+39ejbH/eeTlfdHweTOoPHmQECjSc1rxlcbhrtQ6yUuyLaiK/GqnOE9d76IxFz1LbXKO0my76H+/iK6u3wrDkKQDv8QTWg9hD9Neqq1+O/rM/L2r6lPnq/98cC2mOgXt2gOYJh6BGOKvOYU/6cCNSm11efAaZKGiFJ/t78t97dOeyNgbXKSvr0un5jPXTOwCATnaNbtQ1F7QgFgDIclRlsP49pIwyKprHCA8RspgwZAxV5UDrhZpclQqjat6gUVO7L79EkepQOXMVwKN1vjH4HhEUtCZ+cK+fNp/h0Jf2iAJkgLI7R9H5HAooCMoD10R/X+Du5A0OhHCwaEjsAPVWOP0j6ILlNg+onbTDot19w5w3h7O3qleSFGrkXu8d0IO2dGukfkgz7G3/X1zXZi67JcloNUgF1UgVo4a7okT9KNsTtaNCF+n7a2u5jRk3uKRWnROyBHG/8IDXOt4O+Zpgdm1D4zVaofuiFnnVDgvongc5oYPrdbo8WVXZ6ZstfgaU24/fUCNVSsGf+3N/Dn/4D/9h/J7f83sAAO+//z7GccTz5883f/vuu+/i/ffff+37/MzP/Ax++qd/+tHPW22UPdhiMNVmEXe5pYtRW3kAOqmh/Xv7Ofoe+nuqCwibBdS9F6ARhHnyBEDIimWLNIkZF4p13HiAyh/NJic0mFL4Zd6tx8iZlXZKZMoKCt9VZQaPjD7rsPXDO5cWWQi1g/7yc/sotJS2WD0is4aGlIuyAoso5ToVY/59urHz99WNFoDhNcazCGh2OQIzullUWsnIHI8MF2zjm+ZgNW4eReWsECrZaqH2t49GTRxbfo4VKgHQqOZD65ZLSf0EWhhlDkBiy1lakr60g7IMAbQf2/uT1+e08gOHyhR60xMwOKU8FYRzVpgUgMvm1Dn1qbeWDwr5kklQNWFjEsHwUBC6xnS1F5RHvlCJMJXXMQM1mIEaC0AG6dqj0HyKrocy6DptxeQWbK/2WFxDzgxQ6MUJBDrnTvkP/ncGffnBzU6zbo6e7nkoKSFju7fIHIvAKLsBNA3NeSsATBHC4X19Fu3+ekmgWoBri60YrRxAZeBCAPTXbGu0Xo6/byQU4dYhfABk8VyQWkx/tr2x6h1R8ojPlGjSjhGCoNwHsDHwKElVIfHuygAhKi3QiDe6n7ypJ0hJFvO7B0Aa9ChMKDslhfg1RtactNPfv2BK6jfWSP3ET/wE/u2//bf4F//iX/xfep+f+qmfwje+8Y36/atXr/C1r31Nv+kIDMRUDZWA2oPpdwqg3i5ve031G/iRV4K2ePyzWo5o+9Y1z8WkoqeAHtAV3y7KEgOUcGBtKwjQf88r5HjUXkO7HfjmqlK922c0AyKDfWAgbatxM1Vv0MNw6inZ3n6hh3GcVLCu9vaW93G4rocn3UAFguct4Crtzkp0hfGctdZqHCCFQQsjzBFUDHrwWo3LxWo5OUSG8HaJqjGy+2FVQ3e5KOqp7zU/UKyQWIDJ+kw5B8TJEGLwjr/O6PCyi5Dd4y1S564rZSjOfKxGCpAoCGdCWIxRGIHMAeQ6dqtUgkH1sKcAwJmGetiWURl6JVCttdE1GuwwIUveZ/39cQUf7VS35o7aObkTMO41FxkQDm3uAaAIhldOd233TWWEKzbkPWG5tShqp5p9JQIyFcRd0sByZe0VRTDpJN8g9nH9AQ+lgReTD9I/gAmqdg6kH/pRjSJbwXAlASRVmihDi6JcUsmT/yWylqZUZ8bmwUhT+aBNDKloPoWWYkW2jLzjZiA8J2hz19c69chDdSiAqt/nEF2YSzuHfG12UUYJ3jWbkUcjXrBUFY2qxUc2kTV4ku7c0e8L1NitV4SyAMNedUaFdW3Fk55tWWDrTVRwAMog5cXW2FoQjoAE7dx7+pKun/3HGdNHeo6sB8b8jK1fFdU17IXmX3T8hhmpr3/96/jH//gf45//83+O7/u+76s/f++997AsC168eLGJpj744AO89957r32vaZowTdNv1KU+Gg4TvlYHDXicrwHg2PLG6IVQ6wjgxX89rOLek3jtgh30a1KD4R1iL1g+j+STHKqqeQypHj2hbA5th1v03/17tr/Z5iE+ZUHVnwtqp110G7b7u96TrYaNoIXLVgKwIRn46wi1wNhf69DJBmpzJ8Tm8JEauF+fG9f+ni+v1z+KPuXe+zyoQ0ZEqIr49UuapyhoCWaXCrpwbCSQIsIWWVTIyK65ep5MIGkGZiu/JO3/3TrT762+6tPgbJvHihCUruTBbse9bU46lxkWyXA7/P3GxCZRCmkO1a+X2zpof9hdRjZJMiOD+GfXYlVzAMlgtF4Etb+XGs2EznD0S6n6lxcpAXIjYwxKIjCTLgV3Sv1Luqiou5ZN/WZ/j36tHiX6dPnvCd2ZY/fP5rARvWau243X+s/XoEmXowq++lp2NEI6Qy7dGnXVHiNnPNrr1Mg0np+FI07Btl8tUKbHz+tzxn93IyUi+Mmf/En8w3/4D/FP/+k/xQ/8wA9sfv+DP/iDGIYBv/iLv4gf//EfBwD8h//wH/Bf/+t/xY/8yI/8+j/QwvrNNdDFIdCNqtRb0Bh+j26iWyy+p177d+rhxVkMe5YadeQdY33vBjBvx8VJ6TirrA+6h+yHuGG3VeduTVWFejMCt0La2fER0mLWMbbIwKvh16T/BtA3TyxjRNlZktPVLUyKp2d4+d9Xj9zUmqmI1ht1RbY12X+1r3JPZYwt4nXIIwnCWtpzEkGf2AcRys7YdyJN8HMgyH54nIdMBXxeVEuvM2ASGARTo/f6Kv+82VrJ3xyQb3ctmu6MT33OJgbq8Juzr1yhYr1SuR4nM1BW4+ttJvSeYR60NR281cN7ufGCV8H+Y8Jw31GFfb2Si7UKvM9RbSdhB9h6iJpDEkG4KAuQQCiHAWkfNvOmbdBzjTy8jsl/XjUSrZC8PS9U9p8EjX5UGYPAH0XIR4oOBIPD4kNTAffXAw4BtkggzLqP2HJSfoi707VeEZJp91WjEJXQsazqtZeRwGtUWGuvtG3K3fsBoGLQu8tKubNnOTwVmqV6rVqT6YQffZ2SApQgUlzNZHQilb7WtewSjA3nb2mGoB7crJGHFiKjI0MAlLWOKu0sCuE+Kr04ws14oggKMYjbQeiySU7i8Gi1mLJIv+Z8D7DDyDYndW9WtqBGW9Mr/Zx49C4Bpq6xag7QIdbaDSDjkcbyp43/7kbqJ37iJ/DzP//z+Ef/6B/h5uam5pmePXuG/X6PZ8+e4c/+2T+Lb3zjG3j77bdxe3uLn/zJn8SP/MiP/LqYfZvxKd5w9SYucxruVfc/6j3Iy59/xlD21Pb9hQh5x1huJpAo2yjMuqAjUXNm+7xOT6Yw+Exy1o6tTrc22jvFWA9zOZ61iSER6OoAXO314FpWICU0KSI3iA06LFNA2gcEJgA7hQ4IVQuPF1Xr7inwwox8iPVA5EXbe9T7CQwZI9LtDnkfNLy3Q4itS7DXVfB5NcJHrnR52Q2qCM8EGRlpF+xAyMqYi4x0PdTcBi+lsinJ2hTUuQIaW43I6LGkh2kuSjLhABkjlmejGoJUOq9f14Qm972RDipDUaLCPyUAyxVhve4Ehy0h7ooLAJR2a7pueScV93fNvHgkPTwvBidBOEs1SrVXlh1iMLpzuuJGg09W87JoQTmYka4iltvQ1jWpsvb0Qp0iGVhVJkZGOBcMqTT6c3KmW3fQZ8shFYAHh+kIw10r3vURFlV68OJRd0hK1JYySnFu2n28mlhpNcYKl6UpKCXdRWYZtSbKiQhpr0XA2thRnYYwC4YHqvkRpeVTS+wD7YAWUejY56kaEqeEawFtPGotYtlF5GvTwRxNdSRY592z1LWUTYJK3Cdlv341YMutwqd6fVY7VVQVwu+nDA3qTDuusF+TzCJzwC232hGWPALUFkRo63zg6pRXrUS7PqRe0snmxJzPvDdh6wxML/SMiUdzeMAIS3uGIFVhcamsGpF/gfHf3Uj9rb/1twAAf/SP/tHNz3/u534Of/pP/2kAwF/7a38NzIwf//Ef3xTzflcMD5m/2PxtmDwAdFORF/t5ctOK7TK1HI99VjOWF7p49f1kw9ZDhsJ9fphJAdZVoxzvnySiFPQu59JU4LufbW4EzUD5JTn0UZqBQg+fCLX25P73gClvc40yNvVsDt2U5qFXIw0AWYVuNyFurW3z6yAVzsyatAW3iGJDpav3RluI1u1AkTYf/Z+/BurVn8NqTqi249ADyXMOeoBsejoBm+ij/389pAIgURoR4BIZYHuRQ2AEyOsgAPFrd0ejWI3ca+6nRovdXACbZ7XZA59CvKnrXwDO6jWHpSM6+FK33FtvoNqbWGRkTQAv679qx4J+3my91mVlQsrZ5oq4HegSAVk9xOg+k/TALcLV6F+yUSvtqpu+HgZrzp/tiU2u1w2HRs+coQhJnQO7Dnud6/WRoTwiFhhZ1NUYfe1W3OD1mo6vXf/0+NlSb8DwGofc/9acIyHSPDGg+7sSQaRzPuTXRYr4IuM3BO77vLHb7fCzP/uz+Nmf/dn/ax/meOrrIimfKLPiG9iuxtz+ezwmYHRv9bkYan+m2mLII2kho5giwOLyK/1hacrnwPbnm3xTd5B6BCjS7rvmtzrywqZa/uIQ9t5Pa0I4GlEitQZvXiPVRx8IUGZT5JaTsU2z3o5IB1XSIKPClqDJ5TKo5zq+yi3qmVOtgSKTb1E4yk72lMFnNQZUpPYx8o0grJugBNLrGqnWpPApgUWamGgPpwJqPLixynxO6ThjfGkqG7mThpk0ec5OQkkFcohYnkWsB/UMvYZH26ZLpeq+dpP2B12GkgEYAGuhs/f+6nMnvr6UDCAWhUWN6Ia2joeHgvHF2ijhI0Ng0K3NQzhl7JZSk9heoEm1h1LTgQz2rFSFXyoD1eW4ADtki3FczkAxBYpw6iIIz+t4HtKYeMhNsLWKocKa/Fl0U1W0jU6dR6r6fBU2WgCI7rU8eXTXFBwaRV0wnLRmB9SKaVN3L/FYEE/pcQ7Y1l/vHNTi18Aou4C056oEoQ0AS41qARXXHU7u5KCyMksg5BuqeSYvYO6Zn27gVbnejKdLjwFVcogqlOrekV2r6Qa64HU1dPZ8CridcbauNgoTZqRWi8RLRINjCxAfMsJZRaElEtL1WKFWnxNvU6L5KyPSfDdQ0L8rBgHeW2cz3PkXefS717LNNn9gC0W6v3VDZZsqj9pYjYrql8UISO6ShpbUBTPgigBFmpHvjUtPtHhN4rsW7Upp8j2XzoIbNS/yXdZKU+4FUwmefLXXW+W4M8xcLNPZUMvB2kKIwxJSNx/INtDdotCet5K/uEbPY+kHFdApKcyWM+gcqnERV3Lv6jic3TYwMNxrDyFyCZzLZ+hkDHSRWREtsP6kc3g8f6UTAJdwolJQAmO+JSzP9MBcb8UKaqWLSkwf7XX+mnvDVvRbezmRyscA8lqnyFUQyhhq24086hz7QTF8+w4oBfmtK6zPp5abLVrwGe8W8GlVqOZq1KZ9SRlbnnPTvIM2keRTUy0BYOUN3X0UhY6EgHBSkICTHqTx3B1IXcNRd3Co2L4jVNhO71Msr+bGyaKdgCoJ5fp+ZM6fBHUQPPcUFqjsj1HX2diC8VQQzlqIulyrkdaO2zpPOwC8MKh7cH1tGbE6jRKotkShIki7gLTTAzmeC4b7XI2h0/95yVVVPB8iku2b+Zaw3FhEIgDPNgcraq+rWqaQdV4dkvO2PJQE8V7VbTYRs+2bEhm4ClWHkVepRqpEqmWU/gy2Gn0WwWZBOjAe3tNOC8M9ML5yTT6qggLpdkK6CiYGrI4cJwDiArykpQqRas3f54032ki99h7dQ+6LEOmiVuqzGE71RfLYOH3e5xOa9+cG68KrdsFX3dmhnWRfIALdjNdFjz1zcHNdXSQhLUdSr+fyde6BUTPElS1F7b7dYOXBcxXOgsLGM7u8Fqm1XN3nudHIpUU5RUAWXggJvF1Clalh1IMdgCWuI5jM+7+E8uwzpP7bsJXXsTU/ZW4afKMf60rcCqX07EPUOb9Ujt4Mj+Y/a7xmnaGb5/prr4tD97wAbWvrXnZSxTRv1ldlnpwc4nmvy2vqIWO77l6brl6X/dwjOmd61WsKajS0b5W9zBlyvhV6Fh1azkZ1+Ow1vbH09Rn0s9S4CVzpu79m/+phSvqMZ6Ai07rmNp2sL96rElkEqPVWm30vNadTnPAiXe4HsL5j0uajXoPeI6H9vkK4hM05tSmfKepJkOW2PFJ3qE86xqC+2J6TM/98H3f361Ar+mvpzkrtBoG2Fhy67eYL9jdflOX3RhupzxqVCCFQA/VphunTRs8Cu/zZa/MeaJTLC1klceMFMRHPc4WdEEMrtv08Q+XYsRj8AmwPDouU/G/0uqyI+NLg7qZ2f7m9n3hL+q5AGmyeo8GVtaHiQFj3qmINMTmg1DxlEliDNY1GZOqM32qMRyNliBeRWgdjFchUcV4lNOh9SCkYIoNXa/ERGyV8uY3AbdQk+Z3pJuYC8iZs1qUWDPBhAqcrQERZkZOJ/3YGXWtrLApghpgx3r1QHb08EdZ7quyy9RY1qvLeRphaPqiMYtqOeL3B8nHxu3qQ9etK2jOAGPQXFdqTIdT+VmkyD38WxPsALgVIOv+MhKo4kApAGXxmhWUXsyBeL/eawQmA16W7pp8fTNkuEk5rV4hNSCPueEKFrMJs1Pase4dImrCpEQ5US85IKau+n+YATU5oFMig+7yIRwdWXyWdITBjHI+lrWPToAvnUiFrb09h7wJvqUUFIGtYyEtuMDQB0dhr6cAAWNmPS1erBMDLJGp9FaE2iuSzzosb8zLafJqivJ8xXizrkJxGmAEcqD1LsT1kczwcE2qvPRtlAk7vcI3kav5wVciSvR581fUQFmUESgDCWTDeqfwUJ1EImrcQdJwdvjXyRRYUy8GWgVB+K0RSvgE+81bJFpklH4HO23jNYbB56WfZDDNUfaTkFfmbBnPddQDQkPx4AmLU3Jcv2h7S+7zRFc7qpfgFFEgyeQMfTIo1XqqGOx27X8wiSvHuP8s2a6up8I2tkEDeKS2YinnJjqOvrien6gKUQ1VOAAG8qOagmF5cMVix5oSygEszMGBWVe9cEJhbEaNRg9M+YHkeDHLR+QyLwlbBNq3S4a3Fyn6ANyfS5obc5eIAT3r3fZQAArJgfJkwPFhR5INSnOe3WEVV7YBxskOBRYGwn5u3/2jRfuZa08+XTE0VBbDcnP0+qqEnKvrvqLpr65XCzvkEHAZWGj10HQJ2j65enYoq5zM2pQdgbJAJfR1qK3Svg3PViEqNr4veci8TKowUZj2gXYOOnRxg6ytNqhknQcVqk6mxOwwmAsjYMSQHVZ0HwST31Dgxd8+jHsKaE/XnKmzFwGtp921/X+uPCKj91xwaXfLGKMjASPuA+aCkIe3NdVlm4U6hgDJVdieyRlLatl3Zol57RJbHo2JR4urz1B34pnjCQKtvq7lEAZ8SBqiR8qJaYcL8FuH8Ttmsx3Ak7D4kDEZGIm/BkZTZyEFh1OEug5esXQyM6Sfe9kTE2H2wPKGlAmJzNr7IUQe88Uaqo8PKYyPzqF7KDp+NsGwPz12+OwGE7aHgn6X/6OP57gvNm/T6DF5MrbwXby2dl2U6cgSo3l8ISjWfRjsoOgZdDE3eZxq16BdQJpfT1Z123hmmxzeoHt2vC2rsDD2J4f4LKm24srhyi6ZUC4wrI86pzMpMovp+dW7dcF54oPqgmlHtH4YWm9riTzBjaO9dBmhzvtBFg6T5NhF4a/D+Hjft4Xs4hbS+SwNPtgZ57VBvHjvaP7gtnMZQa6SBTdQdui65vjTIDsYkmi8J1pZBWlQmbPVjIlgPEasdlPW9GEi7gHh7qCytEo3V5vPA3OataKv1SnLRm4eKtkp1SDj1jho1lle3DzwngZlAwYySq0OwGh7Pc/p8b/TsWOfLHkCDn6NGUH2eD8Wjjs5I2nvkkWt+qTqWnQICZQInrlFqU5qX+rqWKmY1ioCuqzHU9e0GJA8A7QilEIBY12y6ajkp7+qtRAwgTebAWNRT4eVL+A9ohc8i+tkW7tEQNLeL0vJpAv13AFxJxptRhvP2bAhnlexSySfPI+l7a92X0us3ah1Wr+oQqj/7VkJgiEQ12N2/P2e80UbKk8IbQ9UfbI7Je5IW24ftY2OgOkMDvH4e+wfRMHGqNFIlDADDnR7au08yxk8W8JK0sHawrnGlKJw1RJTbgxbilgKabxpmHK2GwWoThAnpEJGuTcrEN3yxRoerenf8MGtDxdcN0UjMC4wvf9dX+/ukUeHuwLaDOQH7XCAv7TBaOuLE0CKSMlkEE6iKqg6s0RRWY/6dc/My3eMUbcJY+11ZFIBas4Nav5P3AcnaPSj0FEyVPCAsQ/PSTc4JGBqLzAyosqpyk5PKbX7csQkM4NTIJGEeUAbGcqPwWpmKyh4ZbUxYxWQB27R2iObJDlhbZO5k5R2woDuwAYSVEE8Z4X5G2Q8A6WeGuWBgjQzTnvHwtQOEgeOXGecv6XvEkxJ3hIH7rw44fuUZqIh5+bZ2PT9ATc3f65Q4KXMs3i3wBpeH90WjLVMM10iPa1SSJ5tPUbKC5y18nVb18WK9xKawcQAVagtIe9TIsZEooPmhAVifZYTnC8oSQB8P2qLDKeehzTkVoIyE0zsMSp0zQlogm/fNuai/Ku54AdOLgvFei3vL4A4MwFlr+PLEWK9MW8+cDIX9CKedPp94ZIz3ARBgvVayhPeYKpPuN4mE+blFl2dUpqXX3FERhUONIDLcG0QftJgcAHgNCEsxMsWCsHb1l+icyyKIp4KbbwL7b9PFvUtDRLJgvRmQrtRUXH2Q4T20PL9EWRStIAL2oTocYXEijq9xgWSqEOCmoeVnjDfbSGXxM9N+8PqIqk+Mu9F69LvXRFePhhvDR/UWLRpzz5iTIFi/neEuIb44ahTlfYxMt09EQENE2Q9Ih2EDMfXXokWxaqTmZ6qJ5bAMm6rBeBcw3OvCGAGww4imPLwhABTROirPOXEr6LuMrAh6gJMAAj/cVJ4nHh26kI1mWt5H006zXIQZiDzqPXEyaKAwKCftguvRU5+fs6iHXD0DQJWK6nkRZdSmeXsY9NVt7tRJEQlAhRAWQRxZpYgMoqUiwGqGrxQVsc3KjKA+ErXrC+OgB8cQwOtg0I0l7A1BEauBAgCsBPZU4iDVU+dF5wMk6lnbOnbVihJE6eDHGUwE+j/J+5sYW5YsLRT81jJz970j4vzcm1mZCUUVvNdqqZBoWmIEkx4AEgNmMGSEGAJCDwaIIRISzGBAISGEmKGSGDNACAkhIZBKILXoHoD0uvtRNGRl5s17fiL2j7uZrTdYP2a+I87NW/2KhkO6dG+c2LG3b3dzM1trfetb3zpkEDUwa0sOYd1sr68ZbQLO3yNcv1s1l/EjRj7pe65fOtuKMH8QVXdw6MWcK9+s06o9nbgK5kxaLG39yvLjiq5sHeGeDs2csX73DtuSLBehosJUGtLHqzppSRthwpwwWvPeMWSlL1NERYC3PBFWJmvLArov+OL1CR9PC8qPZ+ST5QfvdWzHaM77XcWctn2i3AHbK1OMsfwWCUBGY9eogrSXl+elbINp5szVmbC+4uhY7NtDuSNcvxBIBvKjOmiA9tvaXkuPDu2oB/2dN8LyU4tmLFpmF7IlzUGltSFflHa+PVgxcdJn66UfvCakl1AS8X1D96ZbRRtJbI4GG3PSDO25Yf56NdKS5oPdWXE1FqocBom3ps6nzQ+HTHsd3PNLe+n4rI3Ut8m7vZhn+gSb6//wEVFVBBFWzyHKqnKChMN33lCvaS4g3UBfkhjI3Yv3g6smn71WBMNDdwqvRgOWd2h9w48IJbHChg4jRouAIXc1TnDpUZLTv8m9AsDoriaXMtQgQSjouG3yiLOLc/p1yZQQygYubTQ0K3R9w8ipMcdGGRATEEnnOqsxGmtCQhqnaeTBWx7gHFt4LgtVSPsG+TMariUaMN48FycISBLI1J+/R0+jkKw9aDjTqRnTXlUuhs9KzznwPNnmbmM9Uag11NkjSN3c8qOqT+STRlPRMsLo2CHXJJbH8Gdrc5huhW8n1vcJgK3LYD1vDmpOS+nzzluW7947jJ8rncSRbHM8WB5remGtCyCFcS0JrTLA0nPBTY0MoB2S64H2bLwgdhjhIncj5UW0aFBhXKvLe4kFODqzaZWItOrB8rUz+kZAQzQ4MuQqPdus3WlhL27efacOm9YhJVCD1iTuKOMDEjKSvZy1F4iC//R9R5/fqF0YrTuiKF+/QzKhTvpAU1MChXmxcB5Am7rB6h3A+xr18fhZx2dtpF46dnVI48QOpo48e+jxFh9jGWqndufwDw0/3TA5ocA80kbD+5uAHk86AfIg4OWnuVzBv7mBk5EZjNTQ7g6orxdg4hBi9TCdtx5hRVO1c1NZkq2Cni76na5CnlM3NACQEtr9ohPJ7mWnAjDmYjyxbGSFmrxWStCEVDsXZAa1jz9gzKCzWE7IGs8xwNcWHlxbMuSobmg6b+DTqoZpmbSWpwn4REpPNyMiS9JE/8dVC5Ovx3gudQbkzhZDlg5hrUA+66aw3SvkQgWYHxump2bkDwIdkuotvhf1/HNCO8yawzKYVPF9W9zuibLoZnkQ1KOuRroy+MyRE9hB02KboHnQbaOI+iKhTsC6EtYvZt0ock96b/eM0y+wwVvdqM3vgfv/rAZ5emqYnqrlPDjyL2G0XjCeUT7hNGQmbA/ZWGKk0M5tJabnN4nA1xpJemFATCGep9Rld0y9BFVA102LsP1UU0JZjjh/X1CXgC8AU1OHQNUtTgmP+QgpDMoaEWldohrF7UFQfmnFdNxQK6OaIjuuCXxiNSqzQOaBOBA1F4go21+GICBLhTW7/NH0VQVXwel7GacfEMq9dINoHYO9fXyoSoBU0/CCnfHzVIEXRN9Gmc2iqfIlBfw50ta1ZYvlg73OcWJUa/cSorswNmXjII5AdJ6Uu2SwZW+AqFGaOivlkFDuFaZd3hWkxy5JFqzJOQFI4E0wv9uQThtkYpN0ItRvGSz8j2Gk3Fvx4/bmX/J4bw/z0r1+gvCyNE5//wsvmbEacX5OuonJaqKyRNojyje2JkCraJcrpBQlSxwWgBMYQHuYrYliN6LalsFyMQtFVTevrbeQXzfIRauIaboxjKJRQ11SFCRSE1Ul96gi3ahDiygDqQGYzQtqZDVCej5tamfXaiKobHkM2qpuUk11+XxBdP02S/qWBvZoaUpBXyfvFWUQUcuM5LJK69YNBenmXg7qIbdZIEtTWOKsEAZVXeQqRApwZSs+tULNiSAbISWD+Zh1cc0JVDkiDwl2ZN/4wQLKAp5VVbOWKWBHAEEkGSFqyWrcQJqMDy92logotjsG1ak/DtLoaXswqrKdm4rq5t3/l03zSpeidHKiaF0h2TahmawgdmC12dFmHjx0oC4EEgJvjJQIAu6RUXMHp+2eobDmCls2STAvyHYDZZAxbWVn9HROELbXDbI00Mag1eejrVGDSes526JQx4CvjGxFvNsr4O2Xj/hdb97jXCZ8vC7YKuPj0wEbL+pJkgDWPbjfPEz+qxtsAB3WbgjjLQnACkwfN6RLweWLB5Q7QXndwBdCOnuR9hhJSTyvtPYOxHH/VVl++WrqKi7MGlGKEizWNxq58dWUKGr/fPR28zUzMGt9fuk16fVp1MugItFOo0498lKH2CLOrHOpHCy/nAk0PD//TJ30e9JKmD9A4eJihKEMyM9DTgro3uBIF/XXATwzUJ88nhmyb/m5AULgopTLMbKiqt4M3WlTMAC9p5PYagOU2Zez9l/yw2BAzfcweOvJacmkUaG3frDvp6vJDdWGHRX99l6ZQ2NPK9w9iSLq1TohxaHHOXXdvl3RH0KVIhhJTdt20+Ydg2sQHHitaqRKp5dT4s56S1qQG2w/o6SjocOILgkz5q1sTDS/YpuOecT60+6D9Do9f+DJazX0+52qLRl0nfT+LJqEySXtvjfp9yrLae8xUSPLGQ6QE5zRNkQbnkMr/RqCJTZDcx5j11QGtjtCsr5EY/O/KC512McgXt5sKKogE9CcRPACF1iYYpOhilCBaBOjPMy7HCRIIBXhRDiBifzcVrTa5mQU/a6gnQDQlfb5RSBycuK5R3uOPa9oY1op4D3VyOz7AVXgw+MRvyGErSasa4I0jag6GkKqjeSKIa6OIT0y2THRyPccMdFo0voqQd9DCFp2IHZN1ebZZN9h10ZNYd56tA/ZwSswPZlEFRTed2PZUp8vWuCsn9mg8yxfJLrnRmkBhmt3uM9TC7D8HSggZ8+pB3TngEqx9QkEScSh4Ho3g0RJJNVJU0CH0wHLe8OU1QH5byUw+//Xw4pOx1t9CeL7mceONIGb8Hp42wsLSdA3g2zFeM5ua0bzbEtC/cEXGk18OAGnsxqo4ol5Bs1Tvw43VFsBv3/Szd2lg5jQHg7YXs1ANoVuqOeUThv46aIkg+Kq3eMNqEdFVvBZjgnlTjtnJjbjIgK6aoRC1mJEvfA0wAX9lG2YkH5wAfKHK/jxunudtgpauf/b2Ick98rwygYRGAzEpYHPxcgkDd7qnaoqoru6N6Zsiu5AOQ4bgUefbjzINnIGyqR/JBP/5Y13z1dbDMzP5lCbWGVfJmdzwuAwM07NNta4Z1J1cy9H8DzQRgNtvV9bOSLgFaXUC7ZXwOMv63XWRSMGYWB+Rzj8RDeUck/Y7tFpv1GLY1FSa6Cy9RIII8rIklBeLb1po9/nbEzJrLp8bJpx5Z6xvtboe/5QMX3QKJaAvviabpKSGDyzERII26sJQlP35hmYPxDm0wrIIL9kTEC+EoRc2Z3CMLGhrVTUwFHVMWYrhfBoNZ8I5T8e8TEf1MD52CwCOTQzFhT5K5kkWIFuGG/VH7xtOhXB8tMNvPVwYKd3R+agmCzT+kqwvdXvTE+M/GQsz4OgPGC3hvIj4fgVMH8ogTagAfWYcH2bQ9DYG03qlysEms+EdCpI5w0oKuUVLXZs7UaDRAIEll9OivqIkXXSuSGLRY/WZLNNjDoz5GBwnQ4r1tcMQLHM7Z6x3ZFFiZ0hCiCgx3zRaI0/QT6+PT5rI+X1EuGF/x80UP+/X4idynqv6ORXnBzwjW1SXbAntkiq9Y6xCQAnVYYYj1qBs0UbTrZgBk0J9DApPEdQGK6KRi6rbkSfFPql7o23udeIaL0LKdQmna2HrZh3LEaB30eZYxOz7nlaBHi66IaYlC6vG6V52+sGXK76XXcHPb/lYiqzLpribd2x9wgbgiYOAJJTp/5OAmfwxbMZh4JFA54MIAtg/YeUaDE8UoNSeU1qJDcrMk7aersuvdp/zDeE1z9cqxMVRtHQdOm9hvwDdaGobVI4STfNekDQ2NuxgV9tIBbU9Yh01cr+NhHoODwHQ980F2aGarMSCJ8KAKQuoLv52TQJ1t9EaEMtVJ210JbEiqZZoT+S1ueFw39GGqKqc6cuFPPFcz31xjjq5wcGmDM0PTocjUftRitdvRcV4GohXIDp/Y3XCYUB26JzAW0wUgSF/iL6JvSoyhe5Og+89fypZG0376SWePzGyqNi17Q0nX9njl5OkpXcMVpCqpbDu5qihUdErDi7GGzaJtUrdLJOqzqvFfJ3KRAJZqqv3TBQBGD4vQFWt2bRmNXm8bWABCj3k/XnGuepCy0r46QuTtPv9ZPaT6rnwaiKEr9+LiIpOz5lnJ5tnv73ASa6PcacFABLenbNOADx8IKF56GzTyigT3Sg4/1EwGQ5J29uWDdE9fut+jKRzRrq1GsAdC1ITxvYYRNPQNfOiqPjQaWPbluse98q0nqW7Y5MhJO6AKTTg4m00NMiKfcSla3j993LALyA12GKsZmjssf6RPXiUf83X+uueaXmV9qw4XF/f3J2X2dH3hZtR51MBbLVp5BdIwRgSwoH0+t2LyOPfFpvXFkFklSN3EVPyTbFvpESZGPUNgENWD4ypg8mJ+Pj17DrNYRhnk5P6PU4lTvzLPZyRmOlbOcrhdED0NmAWRlfIFUkp8uGaIfizEmHxVNCuUvYHvZUq7J0g6Pv6xGzCp+OheiE63cOypgsniRf4/PKQNUxaD4v4BEvoR0nUKKAcCWzJtvfq6STNhU0aHtgvHElSJV4bYSn3MiWe1OisFYZzi5LTynG3PUHqQGovpb9P6dz6zO/vNWIfX4U5Mesgr1zRnk163o66lola2kfz9ej+cHQRlpg7lEdLN+lJScJY4lLuUtYHziU4LX9h9iY2udmwvZ6Qc4MulbwWswx6M8v8qPk0DjFdYrPI9Y9BQwlKt3MAX3+NMCjvt600SGg91AOAE0ESLYWNwyXdnpWo/mJ4zM3UvTM0HxSOeLWUL10eKJxcHCp9RyN4/JUGtK19mSvG75IIMNUw3UX84crWRck070W8a4b0K4a9azrs+iHcgbNM0Kux+qr6OmMbJp2YYAAjaByUsN0NJVrgUmaNI3atqLN/vAa11cqizI9AVwYicQ8/6QT9JDj2uvcqeWRQIYAxeCCIQ/FIROTLZ/Uhkk83ODkgHpBfn95lgeMol4jSzhcsKPIW+dhJ2qMNHAQkD9y1JwEqcXfY5u61wvtoN1iC+5cOnHD7iufk9XekAbFCaERBwH4xEgrgTbg7oeC17+hu/P6OmF90M1yOjXM7xXOcbhP619S9Kiqc6eXlzv1utuZUC76/vlDV29QOrFeezkS1lcJeSLMXzXg/Ueg9fYkRBQsU5kSLl9mXN/erAnbTB2qqbPeH1crBDYYCE3QDgkffnfG4+9SZt0X/4Hw8L+VmCtcm+oYgs0gdLq4MLC9XkBtVlkig8/yqeHhN0wf8k6lnXyzp2Z7/AatGzMY1SNIjwzKg4B++QlvHy44XSecnxbIxkjvMg4/YvCmkFk52mbMpPNGjGXp6ukXrSda30w4/YBw/kHD8hVj+XpCfndBPU54+sGE7Z6wvSLdK65mUGH7j+XNbtU02qz1XhBALgm0eW0hAlZe7znalGyvldDgZQKpErAIagbAgu2ecPrBhHzJmN8VzD896zUMWoSqgm4EhsUgQALcAknr+2hLDHHtAWMCj3nBHSQq0NrQKpEzXRdNL2z3CbxZN+/VOy/jWx2fuZH6GYevO8HLUdV4uIEQgWumq+ft8jMSXUrZtKwwRFd67r6pEhHYawtg7cap1+GQMex8o5XatIPuePlEOmOaMam87mTbQMVguJSirYQbKCRlotW7SQ2qi7b6fdr3tEmZYW3VxRk1Oha5tcw7UdlxPEeNM4Jt6F7wWQfjDRuT1scXwF5LUEQZerfOg3v8ZJRlHvreMGktE2Akj/5gO+QjRs1XRqQ2wBvgDrd5Fc8MJGh87t5PyrXalMFHDNVVI0/o673wpqwuT4BPj2qk6oGD6MKbROsGh1yosjEfzTitOn/KQe/XyRnZ8gi82uZszlE0rwt5KLun1drnJk08KFPMHLLkvc/298+bjhmFZ29rx6Hl1ueA5+TW7xSkE6McOCK1YIX6c7LoqXsUyiQ0+EIT91YIOj9ZI0DiaD0+JvJHOHBfv2TXPAm+fLjgF1+9x0+nO/xECNua0T5kbZ9z0TFoM6GRQ4xDJGVRlhJaGkhE+1a9KdiuE5q1vhBrS1HuFfrSOXVDxBHNnQnLzkhJEnDWQa7uJJE/Q0KdGeVIWjM2q9xkm62RqkkX0RRfAUnAdtSxSmcOWHVECVxkV8es/6GTzfx3cyQyB0Q7KsF7MDweYxdlYZhIrs3XrNedNiir81MpiZvjMzdS3Sh84+HhdnzMvHp/zWVAyri59vc6y83hjWCryc3G658Vk9S5aJ6nHRw4VsMkpq3FrvLgmn231Plpio0FbSBDMEE4KQzIHMnwkA0yJplvRDJZRLck0GHS3jAPs4lEKmV59MhbItXhGsRMqUiHQB2lEsPNzaPmYVyElQwirYGu0FU/9pEaDBmJGu+AAL2XUxg3y02JQOaE9YsF5U7lX6bHGXytuH4xWR6CIKLCrkQAjEGFSWmvTkoYD2EA07ApiW7y2z2DyqLwrjEVZWbUo4nLHhnra4qI5/hjglDSPNFZlTmmJ8vVeB2TGce6MMqDK6/3ecNVgE1VO9SpEeSrXVPSKGl7haAk1wkgK8x0BW1AE9vChOv37rDID9RJGajernYvmTGdBfLOB2MYE4t00hWYzk7E6J44m6wUVcH8TnD4YTblbMH6dlEHadPoOpwAz4E0gKxm0dedJ+ZBuikXUypx6FVreBAdjLfXgnpQuG/6yNowUNTAUgPaR8JXP3mFx/OC9ZrRHidQISzvGdNJrHuwFeFm0t5g1qvK6f8AcH1DmL67YLtn7Tr8tUowtYlQ3h5Q7lJcI29K2JBkTsSGiNbbWTd5vmpkD4Ya9Q9q2dITg6+EdNV8z/VVUsjySHrfE8JZlI68aW8yz8fZGBUirG8SwHcRrbsB4WtDujaAAa5aIqCF4Rz92YQULYA5KLfF/c2YtPpeLSOh1skSDme2AUVW4oagbOp4FIfvf8bxWRspLgJKfWF+85sRcMyo+ecRUjppQ7jdEVDdDQzlhILbgkaLYgD0PAAzqM1KW7bIqmVWuZKUhjxL6pPOv29yuI8h1wa5XvQ7pwmUlBUoAChbpt0iCgH0fZYHalMCrH2Db7DCpEWsJy1GTNdmEUUP6wGEgXYoD4DWDJmMUHrawE9X7LoEJ0Y7zFr4t1bLhel54EoYPumDtmwGNaUQs6TV2I9ex8EEzAmn7004fZ/AW8L8MSOtCnMo7KbesEdHbnBAungDKnF4CAhKrxZrwqjahMtbRjlMcFkaF8V1eO76hvD0i8rQOv6I8OZ/rVoUbF10Q3/RSwa4f5929s2xmAFd4MtPtehRCyHVW08XwWJR/Pom40QcbRzKkQJymT7YfWZgfa1jUKcJ03feIG3KxstPe4xFMmH56Yb5A3eHDMD6KuHyBVudizHZrLOq52hpUwiZ14qHHxbMTym8+dP39Lkcf7xpt1tbS158ylV2axAAtkNSDcSEKFL3qDetYqr7FKy27bsbjm8vWK8T1rRg+qgGYHnUVhLKoFxQlxnTRibzBEwfBMu7psSKJwQB5pEY2ysBrL4OBLSFcP6FhJZ1q8wn4P6iUXJZCOfvL72ZoKlO8GZzzPOdoi29fKNKayd5zO8JXNQgaPNAHf82A5fvWrNHq0t0tqejKmSUfN4Qxf3UVI4JogXrp19QduThXcP8oZkWY0Ey5m1KyjuvDzPK71hQrHuvslUR+T0AJg+lhq7OXVkDx/7dy9cIOFg8+gfiJ2+67lsm1PXbbNyfuZGCGZsxAnmZ6XfzM6An/09rebCVfVTEDGoDLOVHawg9PCA+I0T93J6oZm07oZ1IoUbspt4IwB7qYoISJsyIBWtKYUFiT35bnsnp2XYto2ozRPT7iAyWUHybwyP2NtfoRsNYQDuh2QHik2HWUGmdSeRvNRhJhVsl+kDRLTFkvO7dvXevO+7BG0WytRA/wvrwqNdZl75Q/JLJNwYzQs1rifwyDG3zmhNqgFRblE7zNepyG3IecfkToS2CdmwAJcwfK6Z312DUAUA7TKiHBJeVCSaVw47D4YK3tPWGhKrMLdqeuwrqkTVaNAWSaMcOdCOSnBqvGwkIqBuZEkF3bR1u4eJRFmIup5nDgCrRo4HWhtBsA4b5r5TlWXRM1lessJfso3GfGwQzUJ7DjQdmY+pRp5EBQvrLxi409uaG46IEjcs8hyQUGUkjrWLUdJvvV7uXqzsjQ2QwY58vJQC5QYTRFkE9UkRKvGqfJPH2Iz4eAiXZeJgjfYzDeAGR7wRpbVOyOjFlKJqaxQEhBxUMPr7B10ane3w5+fz0eQHUp76WdgSvpucOB9Qcg8ij25jHfPHfqT8jV3RxpZTxWftY+lxXY0vxDL/N8VkbKW9XELkah02AqGHSP/TPsCf2my0ykWCR6Qdvd45h03QDdlv8+BJLcBR33axHDzPKnDQ0npTw8CwPY5Tt0TB5g8TeyJCUdODU0mb47iC5xOvgMbN+ps1W/W+K5jv9PDuvU9O16A5RHNkqBTVW7w9ByfaGgSFSa80FJRFEGDJnEDOES8+F+P0xQeYJssxxnTG5JzPS3M9dD3nYAPxi7PcCJJAZHV9QSotVyAMRUXubcUANkye5QxECMMq03qdubIazD4Zuek/Ip4Tpg+UsmbDdTdFCu2VE+4YgAIjCgWnF3uhlQjkkUJlCHULvTazXUUO6ZOQTtHI/+aaiLC9nKVIxW++5L9Ph0011ZIfoZpmuLQyUGx4uSpBoVcV4u4io4m9CBEwMb3HCtYGfmvYFI6gyh6kXlIdZXxN0JXSPIH0jbypGO3+0MfY2Fr4ZkvmkyWSvskCuCe8/3qFtSshQkolge+gtSnbUdIvyyj3hNO/r4poVdfOVIFW/H5UAc1o814Q7d+AQcwLom3CQTRrgfdcAfSbzZSD3UI+OqqmgU6Ou1jCrM+j6i8oOVcha2Oa6Gd16UKFcJwLdEhrc0Ck9PKHNB9B3tGbCVTPKwri+sfk5bgnmwIYeo+dASyccWdo9iCYuZBDkJBquSTwifL6Nfur4rI2Us+8CMqHuRUQyO95rMj2lacLapIC8NQTdQnd2Dt+4dwoHY5L+U56/513IaoJq1b5PtKAa40WZeQlKjnAD5BGUSfI40cFrqsSMlr0HtQJW40SDcaPzFXy1YlkzmHxctJjXWgyMCXfNgaCLlrIuwLRaolc6s8cZW7oxKLSn0YptWgFvEZpfT2mWOxGNMJ2OTgS5W1BeLTpWpYWBbVPW85GNc1JRTeG+WIDubacrIMWioup1TGJ1KAhFB6+38RyGsAV01HF/z3tUdIgC6Itdsi7Uw1c6Pod3Em0J1rcZH38xBcwY0dNk0V8FgJtIFQAJoR4ZQI5557knXitoLchTwnLQhPp2Zy0f7D0uCpvEDJNYXsRo6i0BMGq6X5O25BhU8s1o8NownRgtqVZkNKPcGVWOteEFpPr7gnRJljgn1GPW77kqK/ZZlwEjJ+k5KsDQnlgPHJBRdc3BDLSDKojTmdHOi81x0+FjwiYqKpsuwPJOHYy6IIqTy1EjzCjcBeK+05WAtee+NBo0KjsrtCuTAE1zR76Ju15gsjwUV6CmDjXPHwSHr1Ufcrszokry+WTXwIK2UZA+JCkEOH8QzB8lYEk37mzIxiUx6kGUlj4WjK8WWbLlWO/02W93hjy4w8VuWMwAtm5UFJpUBygNdX75anPb0yjoTlHvUN7345HgIgmoRKjfKk/zuRspoBsoj4TdUxkJFWME5KwkMQPl9OhvQ8DwcwAd6viZ12fRFKCtMQD02gc3btyhO6AbqNuDCWi8e4/4NY2omd3Prg7BldidPi7DPQh29y92fULSo1PSKCHkbvx7gB4BDXI3zxpLOsnDDb8bdxro5eQbuH3OWzqYZxYtsz/xmIL40NSWWyClSkXuZZI8c2DGWhm//91P/35y7TLdXJLDPw7foHvP2i9quBfyz4kWTH8iZxxFjwPMMrYvIcvvMVvdkPjzw44avPOmnakGhIESHp6rP0uPaoAgRLBQzJkdrB3noXg/SoPq+zWNyDNBFmOISncKCCbgPEbE4k6Tkix4auDKaLagRxjfYS+nifeoRO/Bv49ZnysXUSPn50hAW6TnCH16bwoJqrOkLN8+SWweZDVSZBUoWsZIAbuG4oQ/T//dNnCqAHkNmj9jGuaOS3r5/BCdX/lqRsrhaOmP7lYJZzx63rUzeOtiPbRsTspu7+g/aXjNkZNdkfVgeEYylY9p3ON4nd9y2xyPz9tIDYtk93LsvRK/O+WSvKdTQzdQDfAi2GdHa/tJYEVuzpZ5drjBa62z8Uxck0QFP7VhnUYTtOhOFgaMSYkQHkX5NXrNVGtaP/WSEasVsEI61KowILNGcDmpfNCkDQh1A+qGyg0XrrrReE7K62SoosOqZjCIBFhVK1BSAhbqquVXpaJTHZorWi4PgKpQHFQKqs05DLe4ZE8irG9nrK/VFXM4q866uTmE5TmYXY2TLRoRWPLc54oZGRIUS/p6ga+/R+E59wqH72E1OnUmbHc9mc2rnqMcCefvzaAGrA/dCCnNv2+IIKCZ5byF+5JFNWltusF7XsALrGvqLC2rteLSjXZsFHV4roPhclYe4GOmRarRRmGYz7Q15LOq3qdrDYkdWA4m1LRN+ksjaPuwO0KmAlIWio3GcyHRkbma/JYArowhROBDL8DmCrg3yhsiGuYrIa2kEN8XFXRX0C4JvOYwzA53cVGIVVYYm1R7UskElUMCgI2CZp/XwTA6y5GAtHJv674bbwRU7KoL/p8/d38uLWvtl0fj0yPvHC+qQP4AU+0XHN41TI9V4eAjh6PmBjCdBYcfUc8VDw6JH21SMoQyF7uj0rIaXqrQsgmLwpxYtKv1qlq/xlUjznz1shqojh/6ex3edeKIR5guKUUNoHWY/N9wfNZGKjzO8RgX2iDD4dBK1LyYcepyJz2yEfN0XUpmZKFhjADcG40LGrzS2iBupCJ5OoFPK1Jm1fwiUlUI85DjOvw7QildDdNwYzf/1ihMSumSS6Wo4ctZqeA5qXzLrO0y1KurQVWnCk36bgDO6OPwgi10kVsi0/eyAkzhGW1WA8yXAr6syuy7GUNhgkwZ9W4OxYyRotwssrp+kXD6vt5rfupN+lSg1q8R0dIiFrpvzHVYKNDNo9xp1Fknge996awGQ2EW9DqUi8oXIRa0JrTLK2shQdpqgaso9Pag4+W5FIjmG8q9AKwkCKrQlt/NYJRhnapenereQQY6sJFQMFkUaAYnXQWzbfB1sTYopLkdvloi3KSsdKMweSTq5+Wt9dKBmFMmAPxUIAQkFy3257M4DEfRaE9yXz9UVXg1qM0moLoAfR0Ovc7iu4sVZjOBtgneMoeLxLPkonVkJOhtLu4J26Hiiy8f8fHxiPYhB9kixrYK+GRzWiQivcKCZnJS7BFPBfKTlmaAjMUWAvQakXnPshD1deiZYfeLcHrcePk+1HLPIXHprEyPvnkDjj8SHN5V8CqYngr4WtGWBOFJoUgnIJAqoC8fdB603PUk6wEB3bcZKOM68Tk9aVNGtkgyn7FznHovOp2f02MBX8sONanHpE0Sk62RydbzsAZ5tbq0prCh1lX+HBipl45duHnD4qPRiHzicAO1f1G68dh9Ge1/vnTeoZBVLDLirYbunJMigOF6B1LBntjQob5+/uENItBEkX1n2288nVSyL359fs3YM3nGPzmDkR1q8u8arkGgXvfY5PEG4kNmyMxot9ErI3JaTjwAdPJzGSKS0Vscb2WAHdxQRCTsnvXwVvJz4Pk59veNWOBy+xq7Z0oxZs+hN+o5sdI9751El/TX+lx94XpEb87rkHx8nCnnquVCZGSbfoO7Xmm354zrsOfJsr9Gv2f0e9bnKj2/aOtkB5mOX+dO0XBPknSeE6OjGe7MeIGyRw5Ah3VjXe9vZff34XsBgKAw6W5cyec6PX9uZrhegqwCJv6G45bEQMP1qtHVKMOjGir+mhiN3roJFGN8ei6ZKeYcmxaeRy0uVwYrxSDfgsaIe7yfBmORojMe6ebvMSYejRu8QLIjVIXK+vCsAqkwB4CqsiNfogG8dPwPYaR2obLv/VFRjSGSsp++EBP1BbvLCA8nz8aH3THSAHgv6fGztQG19iT07iI1uqHHM9j106as7SBKA52vCjj7efy8g6RQvz4JKHP3t1vFimSSSV6/1Rryxy0q0VWkcm8kxrFUzTL9fnZFdABtVrkkp9wiGZT5uCJdWD1iU+SQZcb29oC2WCPBVZmI69sFp1/IaBOwfGhYfqryQ3VitHut5Wq5R0FtAsqwYY2kCQDGqoKuTYfsPOcw1NJpHkOi+6kzldga07UFodTQJmAzajpbdX+6qnK3052VnUXm8RpBYVKvWEjVudWAaL7DVRzypZcwRL8gD6abAMUIDZW0hs2UPJwZJ7Do4Kziny1p7ZRGTRIWuB7YWrEDclY6tjak0+efLoR0LtFKRbxwE4AX3rY5BSohmSPPI4ki31eOCZCDjTdFpMLFugNf2o3epT2bOSsZxggYTga4vp1weaMNHbVnWo9e+GrPZ7b/JgF9zPjp9gZ0YSxPqmoQ8JK4h6/X2iw/5WoaYi00wvBaNOTiy06iiTnmxutms78tUWAjHXBVrT9fq/Njg/xQo14XHQYpLNpmn2MN+Ulbu/PV+rEByBdGq4ztIakSxUxYPgj4qgatHDKub/QZ54tChUJAMtWKZ/nc9/pCWgXzB+m5L5uTbBqT3m5eI7ShVY2RmdYHjaTKUYWZfUymD7CecggSVr42E7Gt+DbH522kxr2bzcOPSSMhY7RL/I5ir0OCfx9lDYvJKdDjQQRvu+4tzOPfXm90ezQBUCGnE3C5gpYZ7TuvUe9m8FrA69bFX+P+9JqfKZrLYLjGvFgTlVZio7ZbvZWG5sroyx+0WrAdJ2xvlh2WrOOITpsdWF18WkEnLQDkZdY8EgCqFcIMaq23CRlv+zjh/IMF64M2Fpw/aH7q9AsZj7/LKLXEWH6qzkS7T1hfpWAGBaU1A2WBJaA9T6HX6xGT5lx8s6deADmoTKghI+QTMD3KLt8oDNRGINsQQ4JmI0xFv9PJEh0CBJBtY3lsSKtS3r0hnOaZdA5OHyvy06YMtNczyh2H16mNAW16maeargAzmdyU5oQcFtZ8gSpUowp4ScBdGjx2jZjqwlgfLG9EjMxqRIoprms5k0ZlCt8kxX7g+UEordzFTi0nJs4Jt9fqoUvwjOSBtInlMIwROs5ZJtRDwvWLHIbc64fWB8L6mkwkFtju9TP5rPCssGrz1cU2vkcGv2NQIeSzzZGtP1/PFQWF2w0KA5iUZaN0er1wr8XTD/tz6XPIyRY7aLkN7xOdX4d3zVrL9+hx+lgxPbZ41pqHA9qizhlvgulDQTqt8H5sKFWhssygjVHuXC5Ju007c/Ly3Qnba4X88gVY3lVlFD6wsvuGQ0srJCKo6amAV1U7ccchXSqmDyt4rWiHjHrMsfc5UaQcKWj/5U7hXarKrJw/OjPQVX2AfK7qsHo65Gccn7eR8mOEv27hn2fvBZ67E3Z8EwT2iXNJG6jicQ2fOD+gC5QGGMyIAs8+Mxi//n2Eb11cAEQosRN59YR6ST3CHK5bgGfwzA6GvGHu6bULBAzCC57RDVTkRagOiYVvYJunt6tuhrfHJvNC4Z9rtD27XgBRowFE5EGAt5VCZ1aN49V/30F5fq4buOTZNBqjuk3fyJuRE5pFo6UpVHpDP98dHu2PeovP/u7jxiCyNioumcO0Y+Dd3t/4WoflPgEBNgJZJ9lnMOEw/rffM57vGatrWK8dMgQCqiL0GjCGdoTN0v9+cz+AEmS8boitnufF8XWj4galwohNQPSsav2tL312ZBQ73Dgy7SIPU2H5NDOWvvZ8NOPz+rypiBJly5CjBvrzt3xsFwPo9++6oWPhc6/Zkqhb28HS4nNV9miTRbSNnLBkhKdmEHLSfYjsWUZfNXNSgkgy/uc1l/49NznDbzo+byNFw0YFKE5rC1isNTTBq/xtIghBvM21U9CBqJcaj11+6gVihRoZ2618VjgevDvR8DQSa21UVvmfuiRABOzeCdE3g7VM2NkCX/CeJ9rBkE0hxNMZdF1394BaMbWGbAWzbiDqIUM4KbxRJfJnskxoQ9+h2Exs0VAR8CmBr1snS9QKuhTMH+q+02cm5HPD3X/RSGI6C+ohoUI70G73Bn9VlbcBaYQT3W+BHTTjkVQU4RoDTyzCySd9NOXOktOsi63OvWbKJaEkI2pHvCBYezp1Nla62qJzI2rGrCwqiKrQSd05AAo5MeRhDpr6bqNrPeL36LUhIer5Ym4Y7NKgJJjvHAAmbPcJmxViTm5UbJPJF8SG5DkeVQ3XzUgLsocCcpiXfdk3ItQk+YQ25Yi460IGX4rVOGkNl8z9vvynS4KNx6hIEV1nuSvAu2SVq5OH8zGchjfC8lNgeW+bsBdbG5znyEC6St+cpTtNLSu7dH5HWL4WhHKCGds26bwYIyYvc9gZaIIx4DQ6mc7mpBSHy56v6xHJSVcBrsNG7vBrVmRElgnb6xn1QNhcDmuI4ABl3x1/ouO4vG/6TABI1qRfC0gOGnVeuoFqLi5AgLdWoaHgX5ISr1QQwCJA1nVUjaCRzsD8TscwBbIhSJeGfKl2zeadfsug4PM2UkBnwqF7dcGm8fcQYjOXJpZDgeZwnOXnxbIAkLiHtMBz4+Sv+eumR9abi1H3gJ+pU7DSwpNq6Wl4z89JGd90EANyE7V4DRXfLIStQLzRHZOpUjBo20wJw/pNTRbGE6lgpm+cxoSsU0a5U/1BKtZ+gahP2iqYnMW41Wi+SOuG/HEDrymYhS3r4pge9bok+yQf2jKgM/p0IzAIaohuxjyCPnPdgSQD9agsKl5JxV6L5hgk2d8n6dwVozXrL1AiwLDZKVSkjQXTFdbMzZyeMFI9b5KvDdPHDVxUgUFcLDZTMPB84wQQEaXmx1rkh9jnksFS3m/MC6PLUQteW1Jj4UWhZIlw/3e6Sv+eiPakG+fMaHM26Fo6HXxTiClKNcj6isXmjigg1Y2oAslVFkav2jZ+I8yMR6+z0unrebWWh+fs/4kTNPaRGxU1UHe/WSCZsD6YPuUY5Yk9N2Oyah7ICoWtrGH+ACwfpJcskM6FctfzLGMk5TktIgTBh4uoeO3mUJrEenHpod1hLDmXw+LxPbG/aEKsLklhuyMPaus3Rupccfhar2/6WBUOBpBMhovMgaszgY0wEmUJ3Oebzg3PgZqhckbn5CxVwBmMPveWd4LjVwox1pmik3FaG9K5PCPWfJvjszZSt7DFyOy7HYNoXAh0xtItDR3YGb2IljD83J30ZU9gB6F55GP5obFTrdchRY3KbwXOexFSfOGzOyj0JrnW3KUcPmfMMoHVQGSODcqJEqHU4TpfvO+RM57LSRdiyuqUrT6rGnNJgApWkmMCRn2/l4zRCNONm5l7sZBO/yYgFhGjJ8BvCw09KQ4Mm7jgOSRBBnf0PTiGXFU4jGzi8EkVKx4WEAsacbCytLZoPLf/N05qg2+I4PJQXvjcq/ppf08+brv8kP37ZV7fM7anbrzUoVzmqF/awbS+mTmc42SlAcbymq5QKXnW6C7pJjf16DngOIfSKrQXk7/uz38YtzBIARH2KMznBm9Q+BKeq9J6n+mkn89nibIAb+sC+Gdl99w9J+dzzLMIXDrMF5BwJojo5rxjNRICwYDomDuUNky53b7VGXZ0A5cbGzLGw9ixS94Z99v5FsX66Pcr4uQcyz8mQ6C4R7eAkmZiTgzlHlFbKd6N4eaRiyJcz17/xPFZGykAu7yETlq7eYP3dGGa9yWGsW4aQUVCEjAYju2c1A1U0Grx3DA8IzQM+C0A8LALeQQ3z6p1lxL4UjD59dzSxeOU0j8/HrcGB1DosQ2vu4EaZZb8/A45GvtvRxAxLLvNjO3NAiUlNKRT2XnbLTNoUWjQEc3Y8HxhFZXzSaJq2enSk66Ou+dQk2C0vATc1ybCZgbIDcwuT2RyNRoxKeuObS7wRgqJZURDv3qUXQsGvw6sbq3QN8ZG4KtuOF5n0pKEV+9GwAkc+Qwcvq7KhnpSiSC0hrT1ehKaE7iogSnHBEw0GGCKqEGs8Dvqz+aM8jAHG1MjQtVbK8ehkNl5CRNhvSmudLoz3QbgbMW819pzHUxaGrDkgSCgzzOKta0ZoI9BPtWQRUpLglh0nS6t6w6eNlXBH45yn0MzbnoULO872aHNpHXsj9qfCxifveapJGmEW+4J1zeql1juYNp/iGaR6aJMs7QJ6ITQI8znhvsf6XPX+jIdh+sXk/bFgt4nndRwFauZ4qbRuT9/N6r5qgQZ3hrazNHGg1e2cdg7sM0LokXAE0dhtY8ZbOxVLqwin1SoeWvaodfXdJsYECVfaINSoM55KIZXJyK667pxSxTaks+iVFJSBhWdi3XRXmHuUIwq9bNFoNOphUH3nmturFvmm7KVnwe4zzfh0TOGPdebKEv/bJbKGphpE7tqeRV+bpBe+F1PeBN9jMe4QSdWIyN+DlZK+6yVgbSWMJRkOn/q8tt5mPfGKywBg4b6ojBkMsy+qElikGsEjkfo7JlxHowwWYJUcw7KGJs/tNh4fSy0Y3HuUcDt4WrxRkf/lHyLMyRlSshvJ627MK849O8iR4DYlNsMlDtBOzSkE6Odu7oEFx0uycoC88LFNssunwGBSSdSeIKwKCqtavyiD1Xu3z+KeTpLanos4IsWMtN163lCE8dlEUhTx4CW/jzinIRg1HlzQbQGmbPCpLMaKN8k6mxN9tyTdbr+jdRNp0bbM9sNPiyxrtZY5hTJ8bakl2EZy5OpQC+bSrvdc0oKbVVTNVlrFAPT6aowcHw3geQB2z2hPKgaeL5oAWtZctxPNvq/F8q25bmxKoeu4lCOBuMdgPVNQ1sE+SODV5236QJtz3NtyKcN6cMF3pMNUKdge8hRqMsXQT6LNke0Pkrk86N29poqrQimUwEVwZbZIC/zCYl3KIsiFRoNa6QioCQDxGaK8U6i8o7gSGhJwJW645YQUFy1InBVk1DvPZ81NxmIhF+DOYOeu9sjF/rGdE5IsByoGf82984D+UnLL7haH7UiCoFWhIXpqikSxJAdOeQbjs/bSAHdcAz7s+egtPYCukGOEN/w2Z2GnHufPERUfozFjn4M0cPu3NTPGZfVBCHCeoPLE5KG2N5zCa0bqk8cz2jptxDjyx96/v6I1KClXxZJiMMMXvhYpUvjDGOgtVRD08gyRpK8N+6fuheP7Jjh7aVb0wW2awNgp3IPjgps4+GAg/xeorDRN3+fJpV2PBY1TJ3VFaKcvrEPOS9njiXr9Du2KQgSiRFJdgQaMZitCsAC7RjshAiTorHXvHtzPIRKBvF1hYc9q6vf9ugoPJPsaeiNLYeDtz3UpxtkQ2PNS/rY+8Y5MgeDmeZj1QQwtYNYSxYpQBg0ZbjiQ1dhEVU5YDUcZWFwlsjxuMMQ9O8KSAFYjM1XU5QEuGr92BjQSRdutIRNRYR9g06ge1N92WpHWUpv3uf0aUAjJYB240mi4xhEEQzzTfr4PxMUhkLAkigca04AFYKszeY6dQfTmlt6w8lkJAsIsN0rAcsNlUPhTnJwmBZ23R0ClSjgHUlA2kpGwlFwSN9rnZq1kFGY1MbCyDhj9+t+swMjEWx5qRcouy8cn7WR2kF95F7H8DeDL3iz3k7OnvJFwgRAoTCl8KZ91DTkrQDYJq6vvShS60W6gEZMibvqgntqs7LkdhTd2sBrUq290kCX1XZlUZz6pToph/XGmilAvzMIG7YpFjxjLoIIVJtuiFU1/qS6YRJjSsLyCiqNw6fLLrIjZkwA+DopnPO0Atc1SAWYspIyfDx5MIh+MCBT0rorI2Ucf9qMhcTaOn3c7xnRQFBWRG+lKLg1A8XWPG57AOodTJ+MolNqbPTSiRNcgPm91re0DGyvtA7FDWKqhPkD8Po3CuYPG9ZXE06/kLRwlxG1QllUggdAL8xGX7MsAt6SQpJ2DU7GaBOh3k9wiBVNUO4z1lcJZfGktm0qotdFsbH0jciN03Q2FfNmTLOb5D1tvUhbo97mj0UhJFLtvvS0RlTXlhyGSJIzvSxqFkFdtLhT860ZySP0+8muTaEriBaLv/6PRSHKe8b5uxxw7C7fZbktiDYc1M3adQMV1rt+x163/IhkQVsasDTUJDgftI5KEmP5oHN3e51RZ4W0jz/ZMP/4CQCC1KOSPgpZCgH51KFMdzQ63V/vsyVWY01qRFhsE7+2nlODGoJ2B6z3bvQtctkEJAlTUxYnJYZSS/XvvDVMHwX5pNqK5+8mfPjdSqZIG2J8JNnHqspH8abzIJ8Q84jX1vUUzZGXpDCl6zKCdM2la8Xdjz5dgKutdLSGa8xfAk5acghEf5Sfi2Jee9oxSdANlYe7gQG753ob8bhqd0rBPBLTktOQ1DxIN1pEPcoRM1BehHsbbfnmvG1w912mhLbsPQip6oFrIWMBNo7aiU8eHg6MDEIevtdxrKq/Sx3+Dii8k5MtMunevisaWHIzWH5bBdati+YCQEqgU0IyYVBcV4V8vPbL/ht1+dQ5uLmVxDomZOyoxxqsL2Ee8iJmpKpERBEbPRAQl8vKQBBFtcKIyKt7jKKb3uZepRdgGv5+Z7JM4pi+inkuX12Rf/QB9P03uHxxNEYahUKBGHQczyei1QYUja69VoQgSDa/PFJRiLWLGNdDQjlQMOnSEDWOhA9hC/5az0Hkc8P0qGoetLbnNOg25EmazQtAi79FIEIdqisV7e4AyhpeUuWIotxQCRDUdElAu5AZhl4cTEWQTwwXlz38+KLw8i8d8fSgLMV80Yg1ojhPHTf13h1aSlfBdqdlC9u9OlT5TECxiCoJaGrABOBYIY2wPc4Rcaz3jPW1jme+JMxfKcTO14LJ1hUP6EAau3eH86rCyjAGp7cwiShUzFjtFDeMvWmNEyMShzpf9YmRbbyC7jJErlrgrdqbly/vcf1SUO4b8hNj+mDG28lIBpW6wEE+VS0vuIloPTcucwa1xZwUBCmDr5p3DAaqsYblMEHmbCzRRXNiZFCorbWR8elHfUnQ+4XjszZSz0JKP3xyeJW3w32ATSR+5s1/8/fQz3xPvNcLNX1DBjQa8sLarYLPJtA4e2M7TRALCdh7CbUGuhbQCVqZTaKGRto3fDug9PS2/x3YGyhnbsX9M2RKahCmZNh099qD2ZXNuDpGTmS5LaXH0dbJJztD7Yu2oUedwA1zzsbZIghtT8/YFUvax6rh78DwN/S/A+gMTxrOn1VUNgIzskixkuUXEAvKZYoC+nM40HNWpJv3/Nii7xZfpRc/vgAtg7mPRVXZI631GkJFoDsdhgaQuGpFdxoA9PwVjFE2MOOcbRgQ0zjWAxognndNCZgA4UkNnssfoXXWbNLrdwFgANZEVDprTyQYjio7NdwX92tuE/dm0uhRSD6bw+Dq83E/9kYeHyBinsWz8fskaHT8lCDrMNcFmJ4I+dL0ed3ZWnXkJTNQrUGoOZNSrP8bIcSQyVEUd4gn3uUB/bscatPx8zVg0DjMyA9zPwaEED3eQtDZGlRSE82Psm7fQkC6EEAMMuMsjOhGrIoSiDnTZgY4x7XAHAwiAtVqe4GRmfK+pk+7N5hT5fdJjkTxPuclN8//5vhG/dDh+LyNlLcL8H2xWTQVUQ469m+v6eL6hMW5jV7YXfBvN5i9roH2SUEirU8CQE9npK0A84TtyzvUo2LJYnhzYd0ghIDp44bpK1L4r1QQNqWyNouS/CHvIqjh2tvz+6SxaNhwYpkyyqsFMikzqBk7KF0NDqga/cgyA7NofdVWlB24TKj3ky4gb8cxHol1wc/JEuklCAFjo8mWNJpM1wq+VjCrurJruTl8VWfVndvudRGmMwwbd1anPcpkj9w3NQLaUSBT3Ts2lcBX63ZblWCxmVxRWgX0ob9VyJpA2jxJj1fc/39aRONeqJrOG7Cp7pq3HgGROgCme8bleVQz1pBI1poyYd3YlncaSvScA6FOEsZ6Ognmj1os2Yb6lID4xBEBM2CXonMqJ7TjpN93zFjfZNSJMJ0apvcb+Fw1z2gOSr2bsL2eAFJG3/x+2zWqhAjyuWD5YAa1OJ0Z0eKDEgBmVDEqfdZ7pyY4/rgBZKSQRTc83vom2zIAKxlQRqTOj7QC7UTxrCVp7mn+wHtGowDHnzQcvlpBa0O5P0TxdlkMaq2C7dWEYjJCvo9olKvEFa4SOSkvzhYyGaxzsyhCFHqDXlNd1ChNp2Ite5KOjamr8CYRsXtbjjZp3Ved1YAfvypIl4Y6cZeiIuDwEwSzcXulY7L8lHD8iV6jMzElEdbXWVEmzzEVz3FV0KYpgHpMaNale7tTCHaeCOma1E8YDIxkRj3kUL0PGP1TSudmlKk8/9NLx2dupPDcewEiPxUsGYum0KA1C2NrCLg3PW6w5lkZqBo+wzdZ/htozj0fAAZ72blNo0/zTHfGILIOtpZ0LEe2SEKQPySgJGMjOhHjBS/9U4fXZwGdgn4rw5TdkHCfaIAlTiUiQ5kyqDXLnenmK1asChLdrF3VfYA+4z2m1xJRzngP5rmhabRJ3BWfva6IqxEqUm+dkOxvWs/TYZFI7g+3KbmB74rK/Ji7V1cGKAWdWAhxbipAKnoONXYGO7l3uG5Ip4s+t3kC3R907pkYrN6fj7nncCwCWYf2F359RDpOTpwgJ/8I8lkVLOqSUEw5gDxBDd3g8knfU1oGDmbYXRZnhIUBdRK20iP5xJFL8tzX/A5W0OnPzcoEZv13fqpm7BTyjsLcTduNhBZfRD3odPsR3rSNjYtgfmyAqNacb3gh70M6n9r4TN3nsuirK1SozuL8vm/QfhzeVaQng63qIUgUqvOo+aRm+ouAGSmhrtYwk8GNnTwQ0QYZMQOI+49oKPVoiUrT5pciuz0sVMsTQBOZeormRkGC9kEJI5II5cBGIiJMT8Zgnd3o63VPT20f0ZoDUA6ez9OoV2yesDM7k0e8aphbBtKqBB6pQzkLALDuG16/1x8OXmT0KgFKvjHKGo/P2kiNye9P/l0w5EhgZItv2NRfOCInEp+TbvTiTQ4b0G6D9p+7h+pRjIfS8Iep/3bGDZkckea1cuSQaN0gl8unjab3mDISCLkU08i0u8mbeEKdiuZ5AJgKt24akhk4Tpqr8nyTkTT4WjtOfRtJjef2iHYYA71h2r0fg9GpMwX12NsAoKnUEVf0pmxMqmCesIMGgyXWADSCFLYcnEKsKKoevr1WA+mebDyT2xROFouwb56x597YnSTE65hy1ErFbSYCXMLKmnD6GFGrWuC/OSQk/dw3mwC7ETU1as9FUOkoAtXeVDAg1+ZtvhPK/YR6SNjuOZS4NZIxyS4Uy384IgEQ7PNTUk8BNnuZQaUhn0ySIO6LAhrz3AiJYLvP2I66DrIASRz5wO4+Q+eu7WFPN2D5BHOcYH24NMqensQYef3gTcJ54k2UKJNglHp7zyqY0ULNoyydMYcRUbdrS86QM6mgPg8l3uc5Jaf4e/FvPoldF4It6M4ZoM6ZmCKKNz10AWZnMFZT6uANmL/W987vlTrPLrRtdVI5ASQ6r0YB3jaxUvotgmpBPJPoPl1dPYW6AFt0UhCfb37O7kwGvX04yg3L+VPHZ22kekIezw2VY+0MIEN10IAXN3aFCG2jYPM4bzZQAcITJaAv9vCY998NZgiczTMaBzMWOaHOHAWDTpnNV8H8XtWIwUB5mCA0K/Z/1QQ4fzgrGeOmNcezAl/PNSxz0Fh3m5xdF5WGdCnWxVb2BZdm6OpxQrXCwHSu4G3WSGctyBdj/W2lq6CzeVWlqoK6b9L+XABIOA7OcrMFWiuEk8F6ZF1FtRiXV8LhK5VfASGgGlnsPYsY1KDf0bIaPS4EWa0DKgnQKDa7ctdQD2akNop2DN7Vd8x35DN1Eg3QGUuJO/WcazAOMWWF06xY2Z2ZlhlYSBXMiz4D2HjCokYn0/QePY75W+sPp5MTrGg5GXFAu+pS0TqlIAB50bg7C8yQQ8b1y0khpQWhZk0VmF4ltImUwXmyBpbesoGsoaFBvZwK2HI/fC3gyxaGwB23HSN23fS13/kGp+8dUO50vFQ12yBCc0IjIgPAoGGeqKHKppPnxBqn6ueLYHmnDQN3y0IQ0Gw+V9z9WMc0XVtE+dNHhWvbkvDxdy1REK4PHeCm1+F5P2/M6Ir9kilawEROvA7Py7pR51OF9YSMDT4o4BZ5KVtP5/vlC81vc5EQ1FWleJ0j00fBw38WUzWvyI9bzzsZtNguKcgddfHicEI9+poxo5wUDpxOFm0loDxYcfCakO5yjwzt/sZ+VC0TNksdeBH0mN+t68+BkQJ8wdqGwd0LBoAO2SHSUJqAfTkCIRHVRxvId+P542DsVBX2f7PJbC0HhBG9ePZfZvmgQVoIBMimMkJ8LWhzQj2k2KTAWTeeSdvHf2OwbAQKiqjtZ9QkFDWo5L2gmmjOyWV4kgqHAujGmpvWC/mGU+quKFIHUDT30SjgQbL7V30kAsbh8UiqqQFqGVG0Wx60aBeNMJ2N4bcQmkXIdRatwm9QjUZD2wjq+britVKDCa5v1+amApwCyMZoa6c2qwdJgNHbOz16gBfdCXEoM/J9tilMaX+PgDLCkimYVzEyi+j3VI1CqCb1bQYppNtaqBGibFa4ma4UygW7NjXORPVrhkbIZdH2Cl4cPBaGUuVuiMVhQyhkSpb7YgK1pGPuosSbceOnrKxZIwKhagE9XIB4exVF29Hl1q93uNmIjqlnlEN/sCGULSR7LzJCuqhenOvX+X0Ld+NJVZAfa5+zTFZw2pBOK1QpV+cggHBanHTj0Qhb00GNalj/fpPL3v3KHTVJlzbcowQ06LCm1+dprZdBbxeyGre+TjySWt6VyO3Stfb9S2yeNQFNKWrvAOqQpDk8Op9gTFlVkaizKpzAc4GEcKioiLFWEePg53IGY5vN6Fr7+J9BAYvjszZSUR3t1HOoFwMYKtGc1o2AHsR1c4A9/ORG5wUo0A1VSNl7+3k/Blhv99ou5zIQFpggrInP7Y46lmxJ+3JYwNsSSVhvdLe7HqIeOTn2MEgfac5LlSZkygo5vQRzMiApQRZVGkh1CwUMaQ3UEqgw6Bj9s21Rat0ZmCHLbDDGBopY3zZHIsAJBEbzFyLIkqwddmcSxvhkZZHtiSBQGZxJFcl93OqsXqtkjZ5kG8bKYBOPmFoB2uabg27mwsD2GqgPXivk90dY3hHy43648lnnkyxTj0jsflGa+gbMwN0yRBLPh13Tc9LzljcHVQFdK1JpaEtGPaSe9xieg7POfIMhuDdPQAXK/QTcT+rtX6vWJxmBBTe9v5zNKK6/ZvCUDhjHbtH7SWm0J9BcahOjzls5QyAZ4Xg4fOzeNsVzgm225Y7BVfMtmvsCpseG/KSQZTlyV6+3/9gaRPJVLYgz7UJ538oY9D6cLGTzcFeW0Q07W2TLW8X81FDeda8/dB2Brghu11BnBk0O6SGUTMb71A/2iCN0Fu0FJ0t4G3qPntsMbPfdgFAlpE26xuDNuYUJ5I1dRdEDOA+sKpScLo4gqXGVBJCYZh9rhJrOVteGjGoyaHF+CFARvcKoChIpEarlHA62Q5ho7vgBMrD5v+n47I2Uy9PAhUV9zhkc4LgvueKD6WV5kV4U91qEpRRpDooyANtoJXIu3TOVHTHh5ur2xmuED43ptd0T1jfW2O1OhlYUen2HHzPe/L8F08c21FdAF37UsQhU/ZxCXR2wzaU1Y+DNkMHI9PPY4kgcNRG8FrCx9zwiQGLwqyU+y2sFW72IzBntbtJEcCLgahJIW+nQUms6lMsMWVTttdxPOH93Qsu6EKbH2pPMUwr6bRQDJgBZ0BbB9mDRDXcPUhigDcjVvUIAJOCrFjI6Y8qdmOkRkYt4+iVGvfe8mUZY6UK4/88N9/950+TxUSVh1EgR2t2sUedWo2CXV1V5luOE7X5RPL8q7HZ7hMMzGoL4IwGtgU9XnWevj5A3k3qiw/TyFhAA0HKKOqlIfGfSPNNBKdTTWUyfrmF+dwU/te64WASTLvq7Q0mw88mUgKJeeFpbMNpaIiPMECg7VZt0LrQh2pBe8Nvr+rh/t8FW19eq76Z1Tz2KWL4yNsIXi5Y8oBtpKoJ0KkgfrwFPA7CSCqXMRx2S54LH+WVwmHafNqdwLTqHARy+WsGrMho9txKNIxeNZtO1IT9toLsc7Fh9Braex4JkQEWXyaNgQ1HsUZQD4fIdwvoKUbDNVZtwrm8b2qEhPyYlc1yHXI/0Z6b3b1E80MlhgK3PFvM3uQrOQ0ZbrFDZSEvpUpE/Xo3kcwDfsQnLuoGGFin7uG2an1a9yRT5QcCcgKpzizcBbc/XxUvHZ22kAl4RhIHqRaBQdtpNwipovma0dr2ZGnZQn3+H/k16dPASzHcbOf2M6waj62BZu/E2m5ee9Dz5MQ2wws31eD4JfRI8Y+357+l5Hx89h43JKGPj97EjQGSMzDBdOC1ySVG8utausuGHn2uMVMl1AQ26cI/K1xBhfx9x74CwarU1a6/QUv+beohAiMBiUKIwmKY1fV+6KNbeMlRSafwuc3DyWTB9uNpCV01BzwNJYouGWq8Ngz4rpe66985INxHLOI67YuzxaKJwmEFowTAU7Dxz33h29PthLSg7y4xvAxws63Jgw/W4Ubn5t55TvfLdGPm68/P5EbR7AW4NdEQQQwRj51MYToyuPmxuAt0EY/31+/fr4NIUJh2HekrGPEWsA4+eQjFcbAi8uLrt1ziZCkc+VXWEJu7s0oV2GzCaRl+39XqwYR+fkRbP216U9nNe14bmYKkCCaQ+dBbILMAkqkOZaWeUXsTPfD/0vW2AgMX2Fmra4FBfJ4QSRVOHlK4VVOt+7GH34tuGndMNn7B6TM8CB5uzXKHaft/i+LyNVCShsVtsgE3wkYVjkRK3phPZ8i+8VYtsOCAvce+wmnflZIJbaOY29/LSYfmgUYFcmWgZ2yvC+taUnBf9SY1Am+tkAemsuHqbEupRBT+pLIrvG3zyYn7M8yKTytjcqly4qrIqGlijukSgNgP1tdGoa1SV09Ywf1RrwheDBL0vF3Qy1uMEMpVsKrN5bBV0vgKtQY4LyuuDUt1nDmyamm7sDGiXVABogvmxWs8iNWprzebdUbCfnNkfnVxdFDSkcxClCvEf1FOPxnoZ4JPuNmlV2R2nMzdTAk9rC7pxOSbgPndigqArDYybvgms8mXrG8hLQfdokBNBYM3nctJ92OYlgF57IlBl+q0FkcNzhsIS3vB0Arjo6w4N1plx/c4B/HpGW+y+r8PliNXIWZ7lVrk7apHcuRH0HIhtTHXRGiBMEsaFptSdH/8xp+h35W3M9Sbsu0SfQ33Q+SSJYt54DRg1QZsT5M0x1iqZYHFbsmlAavTk498ljMzhEICk56/aYQIOkxlcsjYjiLq9OrPOTUscba8zyp3ecz5X4NzRChnHCkOEw4Ryx1gfvGhXx7XO1mjR/YgoxiVM7zUyZEuzOXvVi8/rAn2mTEGW8LkYcKZAlWMSod7NWr8Vz93JMT1pJEsyYWSLCBmRD4w1RgQkoFq9ZUudqi+jsRKPjp/rSH7q+K9upP7G3/gb+Ct/5a/gL/yFv4C/9bf+FgDgcrngL/2lv4Rf+7Vfw/V6xR/7Y38Mf+fv/B18//vf/y2d2xdEM9XfwNDNE26Zem8Tj4QGuM7hO0kJ7WHWwtrh4LWBnrZeqAhgh6eP+Yid5y82Sez3lAKicMNWDxnXL4H6A9sdxKKiSwKfGbwpNXX6uCG9P0O+vEe504ev937XGWG1qtG1ZDQAoz1b9987pRiPR7oA/LiC1mLV9Wqo2jShHLSINp0K8qNqttFWkb866/gai0+8YhbQCb8kjP1wSAC+VuR3DFo31NcHXL8zoSxq3JIV+mnjPSVAqGyR0rDnr1dMj4w6MbjMuD5RwJ1uoEjFOJQBeK8LnTZE91xle2FnPABgOwL10DfE6VExDO1GqkbOW2pw7TT7ejfh+sUUHWm5KMOpzhRtM+ZHY5VVVQfnj8Z+vI10RVSxwyRlYl4lqMBrcocmxQLnzWqmiuZLvMUGfXdSdXCLuBwKTNdm0R2j3Ku8UsuEqxV0KlQlxo6TiGrSakbK+53ZtUn8B6sr6p53/njVXOv98/n2TBXEhqHOjOmktUxp1fMAANfcc84MrG8nuJRUuurazWdt6temhHqXUecp5i2XhjYl1aAzgkBZLFdVuzEsB6sZEq0Dmj7oc6kPs7bZaLDvUQKCrzk+aL6FD2qsLl9o8evyoeHuv5wDDvdxKw8ztlcTvM1Ls7WyPjCuby1/5dB1AsrRWspUf0bqtOYLxZ5XF3WwhCWcs3LU4tu09ahHCVFu0NEdW2Zsr7SdB286Z9Olwqnkui0R2kFVSLxNi0uOOQvTVTVaYqxfzLi+4cjje7HzmJoJUer/HuC+X//1X8ff/bt/F7//9//+3ev/y//yv+Af/+N/jH/0j/4R3rx5gz/35/4c/sSf+BP4l//yX/7WviC8eApPIkgUpKu6Y+59YKMuxQ0Wi+Y3wgCgh/sj9PUS8SCu5RsG3KGV8e0MtEmQ5woRQqvUO68OsjBOHx3v0+nMQgQm1Usj7yVlm6FMyYyUt4a/uSbyMbFzc68DoVkL9th6IRFYDaFrFI7wXZyje4wjbZ+qKAmics9jTOoZst/f8BzH8eS1QkoDakK+CsrVx416O29/pt6wjQ1GcQ+PbyAHuGMj0d1U61PUqYjmbcbgkkQajQuCyh3j1GwgRbpCAgPtMhhFGy/Xtet5mCH6vYXVgMif+nMGEIlnMsV5MnFjMXmd2zYMgEbM1MTGWnd9N9x1JmB1KrPEmPVmhTIQJ6Dz08dyJBwYSoEqCkxRjxrivnYX1YuQYQZVa7sGRe6qqIJvbnXWkoG0NqBIX6NVQLnX9mADOLMOr9G4PXcWDRHjmtEp44IdW9OhUm/9EtGgObZctD+USxY1gwCVYSz7Qm1jEdIIeNjc8vUQRsoFCsbhaz7X+k+QGSmWfW7e582w3oVN8zA9ZyrrNQAUe4+HuBIIQUSB1B+kaxLu4Ntk8LIRXtLmQYLsbijk1v5bF/M+Pj7iT/2pP4W/9/f+Hv7aX/tr8fr79+/x9//+38c//If/EH/4D/9hAMA/+Af/AL/39/5e/Ot//a/xB//gH/zW35FPTTdp5r2NsAnsycqeZ7DN9QXsNphIDgNWK0LdJXqxj6CcODFuPOPP3ReITX42QUr1wOa5oJSEcp2AlZGeWFllJ82brG9m5CWhJdYCSdKamjapCG0z+ycAMCeAD5DEWN+orEunx0ps6n59bckg09zSTrl2W1mla9qF+obilF2jlMdaM9jQqbuSpUNBtrhDG4500fOmlfDz+w2uxO4FgXzt7RIABEyqkcELToKvASDgvrYIClHQk73gt01QMdgk2pfozibCmQbGlgrO+qbRTE2DV33O1FR+KF9unQ5t3Cek/XzKXYpGdLkBowSUz8Fe2JkQSh0enDKj05Qb5neb5jW2Bj4XhAqEkQPcEXEjXhe2+2fANqhR71CLgCWM8i7X5ZdJOt8lk9b1AGhL6hTrIpjP1giTCPX1Eu9/ibXoBrBNJr1l9VhOKBg3xe2ecf3SVB7uCeksofKQL5armRnpmKM8QnXpgI2zfQ9hO3ZVdb0ngkzW1oO6YXdY2J+ZQsZmmFwp3o1xA2irmB435HNFmxn5ouxLXkVrxw43Wyub7h3c8VTHss4qYxRGytT60wXIJ+qNFUmhyTYB0SIGblw6IuC0751RaNr7K62t59zC8en7pFjumgTqbIRiznie1ueGOaRtthysFzGb8phrOLpggWQCDOZLl6YO6Lc4/qsZqT/7Z/8s/vgf/+P4o3/0j+6M1L/5N/8G27bhj/7RPxqv/cqv/Ap++Zd/Gf/qX/2r35KRmp4KiFtI+fikA0ZrDWvC1gZGlRsZdI/WNlYSMdzfVIY98iIH4gU76jF60bD3rorDcOGdkbOalzYT2iK4WzY8Cany+SUhPzKOPxLMj5o/uH6RcUXG/FHhL3JKsuWnEpNKmRCUpryo5tvpF1TdOV2B448blvfjArFizGMGqm4ann9wTwgAstXmqHwNq6hlsznrRqQ0MLQmhkwWySe7NznTPIter3vK02PB9MP3Ks1zXNDudYOjtfQ6Lfsehhlai27GxHlg41Dj480J66JvyifCbNBfmw1GyYJ610B31facrIvdjJr4hpUpCBqSGVJ0Hk3vrvCC0F0NmXHA2wRsd6QFxaRFm85Cc4kiqgIZ+m6NiXMypqpLO/GqBdHw4vGmc1mOC+RuNlFgRAK+zkrV3iXVCZ1kJDoeDDcSw9tGko4pa7TEoIMZ4IXRFr3O+V1Ffq+MuvowY33Q0NTrZsaDnFFmEYvXYSl8KZEv8yLW9RXh8h0lCKSrsi2pavfl6dGM1IEi3+ZrvjGj3ZmxzoRy0M1R65H0XrVjLxksitCXc2q6EyhczHUnYeURzVaRrps+j5yQl0kh7+OE9QUm5vRUMX1YO4M1MTAx6kLYXjcrr2hAFuDKmD5kLO/s425Ms/XEuu21JXsChfeT8sOFh3k1h3t0mCw/pKiDCutybYiGi8P8UfaeGamJ0YyA4uvESyFcNWPst7UZdyCRjnc+bUD5dhz0/ypG6td+7dfwb//tv8Wv//qvP/vbD3/4Q8zzjLdv3+5e//73v48f/vCHL57ver3ieu2Z3Q8fPgAwKCNCf/Mdhg1Mf9pm6r+3/esvHhbG7uqogB45jYdHYEDgsLtoyw2VvdeLOxV+ECRuWvRnUIsrFqe1RfIRvmHZpqzfZefzWg/Dut2bGTuI3pJKOtnBxD7hG5UnWvehuddRRAHubqwkmvkRTGUDrOf1HId9Lpg9pNEBbQW4rlrw6bBrHZwAH0NL+rLZLtVxo564HaIp2OUHnHQbfA3Q3/hoQ09woGDJeB4fL3d4qoBFdDqJK993qEaYICJR46LfKxb1qrVwsd+xHAC2We/mmXU4DhJLk4BgmjVE3EF9w3W8dDjtXeSFQvW4WQzz1q/Zx7Z/Tks2KN73yWOM4uXGGPpb/J4msijWWsRvulaizGS4x5FVKFBD7Z2L22QbtuljsrFmg2TjxKq4L0SBdWzKTjS4Jb5UhMOAAn2WjYBDju7J43iL7wPO6nNnmdwIiZ7bi2TH7/SCf79OVih0nPvugPQ1cTu4GL5zQEhiH5F+foecY5yp5/x3TGn03GRCZ0zePvrb13xu/beC+37jN34Df+Ev/AX803/6T3E4HH5bzvnX//pfx1/9q3/12evpvIGmopX5CX3g4Ru+48gSeaixi+7u30VCDZg3Zf3tinbH6AvYt6SIOijND9DtZ+wQZpTXC7aHjMtbhhw3HKcN1y3jJB0KWz5ULF+tqjhx1GgmrU1Da6sU1+6XBA3hutHSFg0U8BhFbkuvp8N+PaLkrQUUUe5zTDZeG+iqQpySuUNSTZtIChNQbUI36CopBKRmdGV9v5AuYtoq5vf6nelp1XONLUNgz8ep19CJT1vB8pML8nlCm7SpmvesGZl720Zok22efsKmC0ijP2UGSoI2mXxM8R6/ByrGYLK8YNQimZeoDRWtjiwxKCnBYXs9Bd1boRXRYuyLSk5FewaDUMQg3JgnPlWGXE7PD9g8SwmyZNS7WSGxVxPW1ypddH3dYa2Wugp6H1djwxWJfJQkWMF7C1kfh8e59v5HY7+20VFqM6MM9XP57IXceHaMun+8VuRHIGUlMtRFYctyVBmsOhG2Vzp9IIoGTB/I1BQEy8cW30EiqHb/9WjevAmsSgaqwWNUlFQQuUZ3VJLnu/TvXJLCVFdlTgbk5Ur3s6qwUBPQNWPXekYEMjHWV70Q18eDGiM/plC+D7bmKsgnhXzVKFubewzQ3hAlK3nGnoPXXDlRqKlW4fyo6Yqxg7JDfWiizMJFC3O3h4T1gVSaywP7rYV6u0yM7UENL6+CdKlGVOJwAMrCAZ+OEbuLZLsj4aUgABSB+Jb9j37bjdS/+Tf/Bj/60Y/wB/7AH4jXaq34F//iX+Bv/+2/jX/yT/4J1nXFu3fvdtHUb/7mb+IHP/jBi+f8K3/lr+Av/sW/GL9/+PABv/RLvwR6uoCXI3ibrRpfIofh8IqGrtIN1CDmGdCdMV54M4l9N1Clda/eoyPfWN0bSZZngnlTaEreGKE/96IyY32dcfki4foFId0VvJqveFpnu2alPy/vNkxfPVkbjDkqyp2U0CYO9YEGipoEAHYvEgbPhSGDgHHrxTaANyvOrQLgoAYgW7RzUdV2LBNk0vcH5CTmVw3GmAAVwp06bVpVCaAKCh8vSl93CaUw8HZuM4A+5gQGWkH+8QfkHwOSE9rro9Lq54TtIVu9lRlkyz8F9VbM0yMYfR0RhfmirItKKnU9OJjumgTMuKsTWjdtn+IKH8zgct8ZV49mpK4N+VIjhxQCvKyK480o414Iqb/0iEo9+BZwKxGh3i9Yv5jRZsL1VcL1LfXvNYbX2EI9DoOwOqPKjJRrE9p9Uunq5d7iQ3uGUY8uqs29idBeKesun2o0BPR+RLtjiKCoCOhJr6PcZ2z3ugle3xAuX5rA7SLwppTpQljeac708K5ifm80cVPebomwPRDWtxgcNgkPH9yfqW7Yus7IHBhv/bHdUdTCzRWgcw1vX6wXWL2b0GbL4WYOujatBahaBrC+1kLkWBMNyBfGYbLc+bBe0hXIT9SloTJFfi4IFZMaXFdrCMKQRU35BCwfVO08XxryY+2koRvhAiFAZp1/dWas9zp2vArSanqW6CzUltl0HTUXCNK5ES1jSJ2McuzG6YYlE/9yodpddPYtjt92I/VH/sgfwb/7d/9u99qf/tN/Gr/yK7+Cv/yX/zJ+6Zd+CdM04Z/9s3+GP/kn/yQA4N//+3+P//gf/yP+0B/6Qy+ec1kWLMvy7PVg3LjoZSIL1f1BD8YpPkQaUrt3esNSomFzAPAsGhrPI+MG+03HwM6rJppaF4BJUBprrYVgt3GGUsNmPOrx+i0PExuR/2cTxPWzXFUZ6AbOjYrSkzuLC9Xp+a7CAeya91mL+RfH5JaOP0aq49v8PLeivCJBBAinoIl6r8P3+znpWrXNAQDeGCC2XjwUwrL93MAIf9EwRnCop6mDQMMzuCUR9LqgGxo5DYutDZ/3Z8SEdsg6rlkNlTArbXrWJDXH8/ZIZj9fXapGrPgz4F/0zcpJL1pfJ7GZxWlc7UB049c2Hp6P8JqX8QMIiGf3/GA5So+qYsMUfa6khfKEAeYhlzGS2CjhrLHsOUzryTRByTeMyBOO17CrN0p75MSZeA5hAdJ7gNo86OPRvXr/Xc+pjp9GxUq5hssLcb9OImOEil4HmIe9x1AMGb7rJWjLn12xayRSF7f2cQ9j5NH9EI1E5FOkR/1eLUP9833/gZKUkhp2eaG+ND7jl0gdqXDldchNQbTfi/+U/tnb8wc7eWLIi0o9z4/fdiP16tUr/L7f9/t2r93f3+M73/lOvP5n/syfwV/8i38RX375JV6/fo0//+f/PP7QH/pDvyXSBADdxM8rlp8qk6oeMsp9gvZIsZ42tbfH3mGt2Xo4AbYZWuRQbwgTwHNDFJTMF4zULfsPiI1WloTH35nw8f9UIUvDcar46fkOj6cD0hMjnRTSaJnRHhYVufx46bkw/z456OK2TVA3O52gHjUtHxryhQIPX18nZUoddHLOHwUPv3FBerwOtVYCPq042EaSnlarvVJZI3FDYtFk6PGNsFXrC3Vf/q/P65mBE9HveLLP2fcBeKYt59/NpwvkyuCckE6TQSQHbPcTqFL3oM2rHvtDeafdlqGR4RBhqcfpeUFddS6K2axNSga0d1SpkClr62wr4l3eC/IZWD4ozEdVcH07YX1YAOqRGYwB6JBcvhrj6dowf70inTajVGd4MajDgMIqnMrXhrQy5ifeebDU9Lvzpe7yKF1pAUjScPi6z02NoqQTGoKKbR8vHlXpvxtpUj2fC9KphDPl5AKHQDvzkDXaMOi4Piy4fmdBOajTVu40j1qPFkEZQyx/pHAeXCW9LozLF7NFJ6bOzcDh64b5I+02xjorM3CsPwLps54+Su+yPEzJYqSQ7SFBOIGqNpT0gmOx3KwzZlNrkJwgxxz1Y3c/avA+YGzwej63TlywsSURzE8N9aeKjNSDRiU+Dz2iyibrlq7A8rGZIjlF7i2tgjxo8JV7rauLOjc2hiaUCVnuktb0Ja0LnB7FIEOtXyKTbHKl9KhzMoIQZemUe1vrs2tc9kA92r44ocJVgK5vGPUwodz0U/vU8d9EceJv/s2/CWbGn/yTf3JXzPtbPkRA6wb+uoKZwa8OkHxUBeTVqNE3/Y0i+mH1KqKIUqBMNYf5xg3yJUMUyccX/gYgOucOEUbLjMv3BF/8T18DALaa8PG8YLtkTBfVmHO14XSYkB6voI8nyHXtPaGYkaaEesxo4I5Xg8Bi3nIDpseKuQrazLi+SdgOGr2tb0yROAEP/1FAT5d+zUSg8wp+NJ00r40CgE2sXw8Zg88iCh/DBo2G3DkyVfXd0JSbMfFj6zppAHZRrIhBuDl32HS99MS9UduX+hbLF8qka5OyulR6yhLwpLp+EWGYujpgWmJr92oVJrZLcYjPNdgakKekG9NhQn1YjKZOmD82CAPTqSFdNdG1vmI8/iIZfKpOyOjVq/wSwCthOhHmrwE6r8CSgYNCSyGzxATeVKKHagNO3UPn86afeynKBYApo7w5oB4zaG1Ip03r0JLVYiVWGOiQI7LxPEg+WyGoky0MEkxPG/j9aSjtUCeGzIGhlCDHGTLBlEdWbdHxsOD6hrG+cvKHjodLg0lSCGx67DmkerDN7pXBjhVY3qkGIxdgedeCHu1HuWNc3iaDcwn1qEYmn4HpCc96N5VjZwOur4Byr1HN9FGbCgYM7MW1F4UGW9Yu0q68fvzNq+kAWlNIEdW4nJM5HW5FBfnccHhHaFkiNwQgoDzelOmbVkE+NRx+8wQ+rWiHGfX1rJTxoeZou8/ac4qATIJs67clVavXjr9a8AvoGEwrwklKq+X7fI8cCnGb5fi0ASSF2kW+ANO57SJTr01rsz4/pdersVqhzvJ/V606/vk//+e73w+HA371V38Vv/qrv/rb+0VNNxiiATZpL7zPx+YGE93BU9/E/vO3vAT1/YzPCQNT0ovaaoIIQSorPOA1K6yJSY4ut003hzGS+YYjIBj3EskMWR5ZU84KHOArph7FvBDxAAiChKsko1o7EDc+zaGaNuA9dtQh9+J5vpfGzWFFF6Z9YZzDeLmSAxySoIFh528e6LQBQY0nQ89Fma4YWvcAd9+boFFNYoXtZrZmcQiYKfD2EXLxhDFj57l7ETklCS8d2J8nShaS5wz6OYJp54SWgXCyG+fWsIfu7G+NVB+5Nkjj/bXFm925s39njoh553S44+ZR/8BA5NbbxvhGPcJFwRTz/yzy8SJ9UpsfDD4Iho1SIBloN1BvbLCDQxC07aF04dkh/Weso5eGxQue2aImKOEkNOzqMC425tIMgqThOYqy9bgArcBgUQSMq/lNK4sxB5qqtnp3WbBgMdscBtOQurA5kYY5xcM+MUCqznKVTLo3pT6PYy7Yz3jpJhrdvXecO3awkZLk56F9vOL1rBRm26jSpYBXCqn9lyqsvS7FF8pY4Lpj/cWHpEdP/r3WsG931OH9L1wr7GERCWpj1MoohYHNi/cUsisHU5PYGtLIfjODIjyoSPjD98nmhxcxs4bdxVpQlwdBXQTbg7LD0sX7Adgk3ypw1c2HNqMGupfcBxEgMyCpAivtxm93z7vBlw53MAHe48pzTv762ILe35+MXUgNoawxjsmiLKXtdff6Asq7InaqaqnNXgxpcjNniUgqmvplClWKvuAS5ncZtFXU+wmXL6deGOqQHHFsrmkVHH6sz6EuXZNtXLSSgUq2SU2smn2TEivqwihHZYy1CVjeUzTkkylp91wAUxXQZYPglowyQKzmSTPMCXJZq6LPkpISh5rVD7UBHve+Q81qe6gB+SmB2TsGdHQBOVmEllDvZivc5YADNQrH84N0nJAE5U6jKjTS2qgPdh136PVgR/1ere9Lz7Tg6qz1Vm2yjXH1+dCbFUoi1CE3o1pzgumpM97S2T7rzox4NNWQzhvShZBPPt6Wm2vQ52PdB2icy+MR61Z1Fr1QeTq1IDu8VK9F14Jcpe9nU7KolwCo3uSo7ajOVKf3u+SSs6A9t9YWDthXuNP5YUvIO0j42tk5wuN+6I6Glcf4+kkFmD9oQXxZv52V+uyNlBoqxcBJRAUiibqBut00qSdeQ77nxll4ccMdzwHowI+b6UuffUbY2J9KhNR7bQoDpYtOfqUHE/IpYbplwHhNFN9e9MuHdlCF4d2qV9eODfXA2n7iOCHqmQCw55OaetZU+pd48WkYfiLgE/pb8sLYdU+aAU4BGQbVGuhQIqCvs3uB9l7/3n7ScFaUwjxETATwlZCs067kbryczqswb6fwuvClJGV7VYMrfLzrBhXHzYx6SNH3Z++R6m7rHVsP7zRKur5W6I5ouAagK2Ws5u1m3yjYEvjKwKpLF4KlJmhMIQ6aLgweOv/uINzaIFZ/pcl/gM2j1iilqh6dsdWQNPIcnR5nmNZFrwei4zAaJnX+SAvWE5kRZd34CEhzVuJEpj7+t/OGjdWXBfVOgEpo56S0a8CozRpFV8t3UKNQrxiPNhGKGTVctSUMF1Xd9w22mXPhEZzD5UkkoNlkeUq9QI90DAJdi46ZM1KtUSjYYFprWsqrddaW/W1HNFOhhbICbV3/bkV6uu6JX0MncNqKskwByDwBmIFEqpVpuSk4q5cJsnD0iwoH1+e/5+ztby2rUkesFe7R5O5zw7UJ7Yt/o67LoMIezapw9PLTK1IZVI2/4fi8jZSzyFy/jAVWRfrcQN1+dNjoeqHvC4blhXO8CPONx6dgBFhwQwLZUWhgi6fP4H2UtIcNbk/Y1Yh78SHbBOWNVLLlSTfjlhn1qrJLvEk3UEYRBpEpRViBau6e+PMCQXnRGOkYCLQdbrfM4g50a3r+kDDfR01xl0MUEMbJf78lrYhtQCt1lp0Yw9G8YCdQiH1ULNJyzDy0+NwrNI9ZF1jXgQyWH5xU4Cuwn1+9SmN6Of/GrmlEQX2DchiqZYr8RTASm25caBQsVq+XCwguILbnOL8SXNjK/QGBGcImyoExOLBNnWruxeMY7sXvMZQrLCcJfzaAnt9VVTLvIfUhh+uMtyAvQA1hPTFa1loumZvV+o3Fwz1vE2NOsFq4/fpQVqRNmeFenKlG3Ek2EQXQ8B+GzdlPbeOnElsMXiZtDlq411Q1/Z+LDSjF/XbtPHtMMS7jeGmBO/Z7ADOEpOcCnSXp3ZvH8WaYph5HbZyTivx+oyu4j+Uwr8brdWHemM9hrKxoHT3KUqKQRq1CAFlNFRd3nBOaKwn8jOPzNlJQb1uGBn2UbTMZ8f3dB17g54sEYSIqsj1Ksr/7Z58pTvhhFN5dQ0T/7GjsGJhY4RVmgXZnJeSTYHnfLOlPRvckeF8hcNpvQkYl5iLRs2dsXeAaeKqoLqiLbkDlqJ7SdK6YP2zWcXNYRERoDlGUDFrV+9XaILuPWiFOqJBuSPbjMRipgaIdBr5UYNtARDtDR0Twxo07qOp2A/b32DPhrWH+oGOdT4LDuwZeXRNNF912x6aU3pPBvnFtrygWohfzag2LdNgjidWIsEpKQetTppNGF/Hc2OAxAeiqsJAhpJ3qHAZbNRrTaqSZI+Py3QOiU6qxt+5/1OC5s3rQCKW3/4ayQufg3AABAABJREFU6Oasc7jWAT7lUFqvE5umn6BK6hGEMVX7v4fnKBZ9GUzZPJcJrZWROWNXKE+EesiodzkKzDEYZ0r6e77aszqrZBcVwfXLjNNH9fjLnTLzvA7IKfbpMlwaIaJQSi/7htSAZOOure4BYdOTE2h0bfU+scGGw+f333+q9qOpudQJ+V7XhxfL8qUif7gAl2JGqvX9KHKtYlGH7jE7wQ7fNiwSVeZtiX1EFtPALLrvQQTtMKO8mlXP03JiAFTGjJU5uL5mXN6yOT12b4wQg3UZqWdC1B7libIc5/clmK+e46oHjbyAwXAJtKj4a12DdbHITJSg0uYZZfsGb344Pm8j5ZFUGbBuqIcE8WT2y4Yq/hkT02p1Bvw+tP1+1jEmL8f6qhejMCBZBEEe64ttiKeKemCUg8kmkZ5HagNNdk23EUTgwl01g2rTWqLrBhAFBKqe7/D5MQ9nE7vNyXrdAJRYdQFFJYBQjC1ZBwNV7fegLbO+3pyZ57tTsBh2g3Y7QrIjbwyTmBjgIfk+tj4hAkrDdG5omXF433D3n07g0wY5Tij3k9G9sy7aJIM0jrEdLffUqnnrlqdKV4mF28T6WCWgmiBuOlckACQJdU56mxaZQQDxvlaukSfDfxYlaQsGnQf1RlIHUCM2PRbw2lAXaz/hSfCBRCCZe6TqzzYnzWFGdGMQ3wStUxmMYd9gfF74M+mEFP/PSxsi6kZVj59UlaEuDIeSIm/BvfBdI1BtDb/85hPosoHXBwgtmkt6TXANQ2+74rRq3/TbpOlJAF3PbjeZ1LhFBOtEDB7yLAMEPD4f3vaSUU7kaNaoVJhAhdEmzSGlVZl+U6zFqhFWbfp9U4LM5kg7cnErMbZ76PY8SWWhyOsGLR8NANRYCS+T7RkzIV0FqdXdnFD4jlAefD5RRDgtAd5dwYuHncV4m3tKa8P0YVPGYnbCkLb38bUUBrAq7T4/bkAilGO2fJg6c+X43xm777/a0exBm5dB1byWBqhWHO829Nik2zA47lmMRItR+RzohiEgFT9hj452btztZ82b1TbWSppoQphSBRZgm8XyRZqDoKbQHwmAKYMOB9AyK/acU9SDtUEKipoKdaZVq+ZBWjAXR+TfqN/zNjCQAk6xsSCYvl5XJJesq5wA0yprkNSA6pCPr/whd0XUo6lnkEfbf86u01VDRIbPDK8DMMPIYdCoVhUrvUqwmzA0IXRVhXQVsEnh1E+hDeY7eE6p1wtZoaxL5vh4QuG4dJV9gajoc/RmifkqaKfukbesG1addUNhL8A2iHGEfHlL5rWy9RXbf095SAAfugJC5DeKFRBbtLcojlPnHil4LQ9sI7qtHRqjv5BbskCgLda1mXQrkYmxvU5Y761Q2Wp8HBJNSRmRXnOln0mxhl2RIJ/7/aezdLUDhzwtWnUatD+zMQ8yQuF7eGv/rEPFoSJo6f63OIbkvxvdtCk1nIxEEVEMka4VI/u40r3W0w1hk6M/YRjdybRxsTVLWS2kRr6DozrCrcCznJxCnXrztwW6Ot8Ubk2roQXWGiX2n5tAh+zZwKBcmTq8GNqfA1Ow5d43K1qQoI+x/PfQT+q/+tFa9+4BgNkYaYNBsdclNrlZaZsOBxoeT6smInfHkHzuWLB/BpYD88mCLjp5k9eSw6TK5Qd9SteakEjw5nhB5ob/7TJhfXuPyymZVyahJN3uj6Apo80T5DhZrdWCxx+kkEtpljRe3gnmjya133LH/b3lgCA2/XRtyOdN79vviwFaC9Lq3Xgr6GpSN8sEuTNqnNeROe15pK3fRo9j5Df+/RYOHWFUz029dD6LEqSauwYA0kCn2aAIZXnp4vBCV73//ATwqoWW17dJCQA30JZvdg5J5SfVKrN0Frg0pFPRFtkDrMxb1nPnYQcUMSJGizFf3imV/Po2YbvXZ7e+UeYlb8D8Xjdob8sgSb1ebeDpTD+KhoVseczL2wThHOPm9OX7HxYcfnRGWzLWV4zLlxQRgbDWuBx/3KwWSu+XwwDrvalKvunsHRjOIVAJoAkgVV2vi0aB17dKWBiLUtNVsNwx8kXbaHgkRQKUVzOo6dxOl6ZafU+A/IQ6Hbs0ODvRN71QfzAHRHUJ1RB7nZdGPTBotG+UPqvSxZidVYkV+dTifsqBginqAqr+YS6C6SSYPpRdU0xqokZ38s1ZHbRoR0MWgUZxtud5RLtwm4ZmqDJUgXjH69HQJAJmbYQliQOu77C9/ZDhNTPWkpUwgpWQzxXL+6pOkY0nm+Yk1xYEHiFYPpRRWaPlZhG4zk08qzgpd+pQ9by3GrPppPAw//dczPvbdjQBpPachtdiAD2CAdR4efFprhZNwWjkEuwchw2DMTbmtbwI7xlmK7to7SVWn9a5pGjvXWpCygX304q7vOI3jw/YDvcoRxPTPDfwVaM6Wax6f1ZBSJkY21EFOJsVpLZZFxmPxao2S7lKVOaHPIt7sQ7hsYByUrHRFxhEIFLtPuspJNwit0Ct6fibjNOLJA83VAMcupP/GfKE5NHxOJbje5khxXRk4m+qZpAuJRanMCm8Iog8IdeqwplJ+xUBg0KBX0osJvOQN8u3WI0XlQa6FK1ToQG+EkGWLqYZt+7QjgBZas9pLQuqkSPqoj2F+KpQjPbgMeHVDDjNmgthuwO2B0QSOllfq7oYvZ7s2YvCiPNHxmJK6VqKoJ8td0pOaB8J8zuyjVo3W15LXwMEkIVtrjrA2fNXQFvU6K4PmkdqE2F9Y4WwDUhX1VRss21kSckt+dQikqpLio2MrZ2H6uG1HXsMQLAfQQZTmrJDvmjdUJsT+CGjzQ6bqkGvvI9OdxGRyUhpO41ikdikkK4Q4J2/xyndjIJ+qeDSFAIrTdfqpHmjKJR2xYdEBnkaTBvnsvW7aqdhnbsK0+o6agAnhc+tkFrPD7U2hIjgPqks7kvKI01SJyZtgvxUdGwsMuJVqfW0VS2FWHJQyT1/Gdp9RsLwLgCxngmDqgoFogAAZIXDzxRlPnF83kbqm44xPzQSF2qzthBAFJx6K4Tadh695h/ohZO/8F1Ah/zGDZqtx8zbGdu9PtRSGYkZxWC/1riz0KpKjmABtpQjGQpgl9imio6uEYDkfYSwgyl4I5V1ubSIpGIzeKExXYzbrbFl9wQVRiNJ8JqQIIuY/t9uTMajYSCk7I3Ui4e/7hp+LrmUc/wtHJTJZWn252IvcDVDoaoJHKwyl0RKQBhvqhZFnbRt+Fj7ovqGXRyX/DpLA3PrkIzNI2fMjV6mz6nJGvnVDyb/Y3krl5vhqrY42tHYxhqwThu8VzO2TqMOQVUgCjjDu27A9Ki/pCtswyeQMOqaQ08vrtuLboOZJ4gcDrArCnUFDb+ftPbr2O5Vt3I6afEqrnpv+VLj2ThDjo10EFC+jbOWbagzQNPQh6tptMWkajPaY0nvqSVE1KbSPkBbbJkwLCejSi90n81xMDackQuaRWR1MeRCKOBaf6YE7iUEPg+dcdtU8QFAL6THQEQZyUP+T/Go2IrniRRetfSCIxhtSVHfxEIgq6sKqHtOaojXYe9oCKmnNnPkpVzAmmoy9iOHgWqJe/kAFJWAjV3XU0RQzmPvtHnppT6fKo7+1PF5GykZrHe8JPu/w94iohtka904Ga2YqkVRRaEvzyH9zCMWqex+7q4nJVy/O+PxdyQ1UnPFZZ1QhXCcNhxSwbYlHC7KhhE2fT1mgytUhyuf9e/U1IhFF11TvxYA24PsFg6gzKi7nwjmr/c1CbxWlSPytuaWa0K1Pk8ivV9OSmhzRrnLOyaYwjnNlOZ7tAHPd7mhs41HW9wPpAonpoiExXVMXP/dIzBNHPszTQCmvnkB2gDwBWYaGkIey42pMIPXOeo8pieJYt60NvNqG6YPV2026GPhz9k2h3FzocbB5JIpQRbPISWsr1xE1TzLCiwfKw5fbcq0fEqaI5sJ6xvdyHlTNlUksKUboHTtEYF7xSON2mV7gnpvUbwXcaYVWL4SzE8Seo7ra0JZCS1PXerHIgjXhgtB0zGXEIZPWaqpqn6hMyWT6RJe3jI+/h5ge9swvWOgMZaPgumxIj8a7Dw4e94BGkCoe+gcgs6/KenmfGSTA1MZNKlNI9rECFkf0lze+iqhZdWOK3fQIuqrRYQgrA8aEYKAcvAWIqYpOANtEpQHrTNsP0l4+E89cvHva1nlpYRIIyPvogz0+3N4PRkT8pD2sBx6BE7FinhLQz1Oqvp/4N5CRjSa3Y563cs70dYwW5/zMinZphzMMTEZJBdA3h5yrBcSUfHfPA35ansss8LNIO2KPn1Y1RiG8TIBXnPMovlis3lCHr5iz0L8GcdnbaREXqjTeSncJdscyfqb7LyWAU6S/r4djPcpT39/MS9HD6z9VrYHzTsIC2olpMQQIW21IQiBx7pQbGh1sQ6iST+XrhStpKmh15h4wetEqNhfA28GFV29h4Xdy6d09MYNefw7D1HBwOQRYkgdlJ+9gBAwI0cgWB6peWsLGCj+M44hQSxR8NJfG98nmZ8/J0I8U/fKtVjVvVN9T1q1nT0VJUTQpv/m86bU+yYvjlW0KiGyexXdsxNpkbbBOt5KRDc6hfOWj1CoiHTTbxOh3CfrD2WG2hLY4eX69xoleBRN7ffbo6hdAt6NmqFH85NgeVc193L0bsyCYpBxy8p6dMg7bbrBOD3fv69LT/XvzlcxVXqE0O52x6hHAd5sKNuslOSzbVhbBV22Pv/sZzgDkkDJd8oXnrF/f9EIilmLl/fPKgdEtRUT003WPNJIFmMzxDrTrlVGmxWCbccGuiuoh961GgC8w4Ak0j5hvnXUQaz6hgQkzKApdfbjeIjovbqTZ3nNZs5My1qU7IxQp9GDbBw2092sFdSyqk+s5vBeh5YtEUXD9kZT8Mg6j0P6yeZbM/gZQBhPyQypTqJIABqaFpT1eUJD1P+J9fSp47M2UnF8ijXmv1ZoTqpWdTlL3W9on2LzxQlEvTsrrNMiwV7c6Z8RZlAewGYAMmesr60V9kHArzYcDxvmXDGlikxK5dJkZYXkhDoD1QRhta4HpjrcJ1BfQEanJkCSAAvUi107XRpALzRk9/x7ywxlRdZunFNnzT2j64u1abBwXjKULBmsL1P09giD7XHY+InNVHJv8jan5wYJ6BHtrSPhz8SvzxYyb9osUfH/vlHs3meLP10b5kc9R3jznnNyCLP1iCmiu5wUWnTP/iZn4kQKh1Z505ygQ04tqxebLgaV+uU1NpIEa78jY1J57Yon/XUTRL+3Zg6Lb4qiMj75JEib0oDTtWqEamoKXP0aNGGeL/o8eUMYF/j8JnWe3Nh4FK1fpt8tDaADhdFyOZwGAU2kHKYmmN8zVpqRH1W1PzZXEz+9Xccv9htySJcsd3Jh7XhQhjVvZIsOVTpMiaD8T086lvkCBKFg2EgDvhRAVoCqMQk5oV4Z0yODWlXG7nhEMbRGVzJniLfBKft9yeeP54kh0HWajWgxq6Ynb4ZoDNem+Uhztqrjf6TFwzYfdUzNoBjzFbBcmlPQZ4J3gfBnCtxEOmaAAWNCktVjujG0YuboHi6skPKTts7xDuPbPdBWZaqqcsjPQzFvG1wQ2U/S3SEVYoWlxKVTo8f3+2vMeLEeKUgGhj2PJC5fBIkgvB/4epxw+h4h/c8fcZgqHg5X3E0bMjUc84bMKh6bz8D8fkVbDih3qsJcjwovSNZJfG4Uyeh01u9U4kRTaGNu4KlCKqM+ZqST17+I5lGA7omal6W5lbYfEyKVufHc1E2E4i3qAZhK+P7vrpyRrtVgPd2AHHoBwYxB1rW1qWBmtCSxaxDmyEdF3sufww05hbYCfoRW1y8Zcj9py4mRAGDPkVpD/rjiaPBGftxU+X1kHI4/qbOxME8orw+QmdUwDkKine5u0VsD0qlg9r8FEcHqni7GonRmpcyYThpJefGoR1/lDkG3Dhu+upyTUbwtkpofBdOTEhOm95u1e1mQHOIsCtfkxxVUMtqizEGqEs+OmSyK0yhzfr/pM5xZCQVmVFTJg+N6fU6MHVnF6PV3/0WwfNUXTjkC9WwyWNYw9BtrE/05mIPD14LJnQpninnkwWLRCsX0dILC9NTUKditYfTI0MbRpZCiSy3rxqsQrRr7Nt9stIOhaxMDmOC9tnRDB+hmXel8gb5HRFHqmVGOKdqksEOYdo9pbZg+ajF+XRLSquSGfLJxYLvgDIAZvFZMj/qc8kVzrZKs6+6Cns8lgGDFyZtGSQp76rW60ny6GPN3Kxqge55wWyCHjESEdNHoqt5lfPxdkzZ6vZrzzEDZfh6MlLRnUdNugo+eGUNDquoLonsJz+SQnn2PdE+8AaG18gxeomevSSbUg+DN/QWHXHDMG6ZUwSTIrD8B81zWouvUWXsT0JYGJIFnHEXL9kGOjiTRe2MBTxXTUlBLQslJE5pDAjfGwe/J1SxS2hvqT8GbDrf5vQLPFzoQk1ANw8CSJPQ8kw8ZQ43hSPTxa+D+b2md6aXfMTDpIldUFV4cvVtGFEXq/Rm7sGpbdwDa5+i67aMz3FzryC6zzqa8NR22m0wwSet5uCqWYB7+3iQ8UPiImnabCtwOmybZfLBWB55n68XAxtqER8jqNafVIDdvGWGtU3b/mYHlVcBJOkwoVocEzflxEXMkGhpPpjwmsYR2tGePStjnAUX+UYujEVC2FiRLRDvKzL1BNF44olzCc6A+d9nHX6FlpCHCHQ7PlSHG1p+xj7nPK/RnWP2eSAP8q+wMcz/53rEVk6KCaAsTkEDA3WkEwjCPQteOUnjPuIBsgSiR8B5enAhiQrKjcozKf3VDyEUiX8xbUx5S0wemz3xYl61D47tnKwiUYNfA1J3RWgGLqthSJ95vzWHs5qScbwP547M3Ui8Yl9EwjQbMHa0h2b6jqQMY28LHW6xW4CWjFMSAcXPzzcwWkqusl8qoSQt4X08XTdTWhEuZUAur6OVaLNGtQrCSNc+jeytpZb9N0HoAQIK2qNeIJKAkSEkjMyTTP5vGRSfD5tmJABGdOCttNGyeyyvWbE0MYqTuuY96XX0syXIz6LDB7dgKIl8URoK610ZjG5Eh+TzWhDyXcTGvdGtgMJ5+sODxFxltBg5fCe5/qBI8vgmQ1lOr0TCiiHeY3UXWQ1EmXFx0a+DL9iKM4/fLa+l1ZNzHla+le//coRKzegioz/Ik+sx0vEe9u3qg/b7eFLarG5n0z4y8JJRDsl5A+pzKkVHeLDo/q9YHjZskiyCddeNMlxrkE462KK6srRGJ5p8QhrXOCk3n4VnlCwBo/g2kPZScALBbSzfOAsb5Ox7sxdrUnUh/v0VdzlQsh2TECVhyHx2adHkoj6J28wlgIjTyppAwnUejYS/7CTi2pfCO4VRFazNdBogBQYL3tGuTwp+tCVhSQIj+vEfR4ZDWYqAdc6ifU5FwVNpBa9HIpN4kEeoxY3tQhpV4Ia5Hpu6/Wp5KjZndQ9Go6rbdBllaQNCZtkiM+vqAcj/FM3el+cN7bdLJVVTh/SqQnwtZpFI0ChiVDl4gTogTImAbWyn6GWskGHpxbrDGzdRgG530PdxGFSvg7VCapAS4VzZIkgAIuvkhbfhyPuHaMn5cHvBUZsg1IZ8b6HQBNdGWGq9qGCYupGre1ml3uxfUV1rfpP8BxGqgcmogAra5QupQy3C7AZi80W2EAmsiqLReM2RVPbZ0KRDTfwsmmRsh80IBwCVzWtIIRnsg3US11ca2CrAV4LqqFzY6EcD+mfhP7s5F5EkYARdSreCS0AB8/GXGmz/yQ/ye1z/Fv/p//U+4/Lsj8skWunhl/Yx00roQjJ2BmXdtJ2BSRNQE2DQS48frvmEjoB17p6zR7SadpTawFZ1ZqUy1DEpscA8il+jRtM5b+1k7nbrOyjzbCdY2aB7DWpuXo4ZgSoQgOG19uyMAM9LaooBXSxk0qmE3wJa0j3tsUA+ZGW2eUQ/2/qLN+SQpS64c9DVP7nPVPBxvDeWoNUB1Nu1Jn2etr9PuJJC9LvsoBejFssBzApVt7m3WNVjuGZe3FLVnty1YfN4+r5eD5rSrOp3eMBRQViS1WyPlEbjlgS4tCAhRm2VtfjS64GiDolHo4KQ5YSFZ8WwQGfQ7tvsMVwlJ56LM39x1Jb3TuDBhe5Vwfa1wX5tIhZgjErZ7dVHqYrkuz/VeejQlg6MlU9L9wsVtM+P65YLrWx2gdHUVFsHxh1c8XK1oeFLmK5Wfh1YdgE3ehlFaJxrixXt61CBV43YiiQUQYp/As3B993P8Ttv4u4y+vQb3pnqCdzyYBGw7ThHGVlOHb2wxesGd4tS+6XRKMR/26JiXHL2EWN5Wgb907NiOQxPBfQ8p84r8pRc8Trr9Pl975gH2QkKDdm7hHfue3TH2jYrz3hiulw7Ll9QZ+D2vf4r/y6v/L/6fr36Ay/EYtUaqB2eGPCtBw1uTCFP0ddq1n/DkcDNHxWvsdoZ+uNbx760bKXJ6vxst35DHobAN1A0XgEjm3xZnxr8JocbgrbciSuAe9YZIaiRQROtixvHbRmaaOXlic8A3Wa/ba2qIAsAcnBb9QnR40ejPO6ZgPNsXxuMlSS0MCIF/Lv5gkzF1hYqWvQAVCqNZS/nd0fr1CHrU2efxMN7j78M1xFjbMOhcl9179No1igpqtl3zCOvtx2X4p4MKXpvk3Z4H2NUVJADu929wsUeCUdzv5x0MVTgN0Kh6/D5/L4gUUg0dSWstM1GsMbYuAFwa+OmqpTVHWC3Wz0EkJbUqKy3I+IjFJMA+asBgvGrVpLwIiIpi4k26KkUy3a0xVzW2JBg2E6U+v7CR2eRznbb7ZcWSKr663OPr6x22mvD16Yh1y+BzUorrmwe0iTF91KgsnSnaY3ueAQK0HwF11qK+7UHVA1RFYMKHQ1PP9cpIm7bgVv25wWCKGNSVew3Hs8HVexeRXkg5LqAXNsmeyxGjXe8To6NwKRdGygRUQZoSaJ721+JwTQjJDid66ZINOukV/Vq7cfip4F/+P/7P+NcPvwftxwfcXaD9o1aYXJRSeK/fPWpr9o8r+JQhh4zzD+5wfcO93mezhPW7a29AN+bxRvKN15NNUNjS5+GQM/Aoqr06oB0mlPuM7Y5R7tQQaoty7Db80eCoKLGdekY0dJSkhbP+79CxE4TgqkYEBD5oDdVYs0IC5CdW2Zq1qCGzeyuvD7h+V0VgtzvrpdWA+aNoHV8BpieTPGrqSad1r0enxJGmLag20caISza2mEX2rubuEdRNLtPnk4DCgYp7MFX2siy4fCdbXzBrZR4Qn44lbzB9QXUC1RHcG0Rfz2JFsGJj70oVejGAkCCvmoP0RoJkuTvto8W6nXCPlrTPlFpGrzGUKUEOGvFwEcDGkgGItf5wg+ZwJVgtk8qgKZGqZUZ5UCRgO3qzSkHbXMZIwEJKMvE1xTTA2QMZBQCYQa6HOebpbc6gNkyPdRgPq52agO31jOowpHVqeNa+5BPHZ22kFB66YXoNG50M3kB/rUYNBaWkHlNiK6nJcY5o9jfCTON/fr4pWe8fCjhI/6ATVFWTBV8czmhC+NHjAx5PC1pj1HMGCmmrhxko3zlCEmF5J8gXwvxBcPejokyawcPRIsENkhnnX3zA0/cz2mRaWce9GzY9QmmpNwYDpWqvqNvkpauaD+MngxEKJqOMnvttBNCr6KPNg+U+PJ/CRZBmaxtRMvhh6ovazuHdRP13/V4JIc7dQZYAH3MTAO5+1JD+dUbLCnn5I04XLeLVaItw/m4C1YTDxJgSY3s94f3/nHH6gSBdCIefatuO5SNh/qmAni6xOT7LnXjxaTJPVgczEt1oalAJAHJCebVge5VDl2+7101w/qAbvBZJ6j0qxEfGNBMcvlY4+vqKsb4hi6KMmEAWMSRdJtOjirVqsWqvvVIjZFGaeb6HTJg+pt4O3ebB+nbGh1/OahRnGKXeioc3CQhXUtfdC1IAEwT67Oaz5iBlUoYZSOdEdKM9b6AnW9sNz6LMPtnMCAxj64og8uUBp+8xrl+YV2+SYVrSoeNAF+2tpuOj+ZJQeDeDVg12FQJgCh3KrDR5H5t7BO1kMP/4CXQtCvtaaw3t/8WRp1boDsiPK9KjFtr7nFF+0hSpgiS2hlmRGHGD5zk06oogyvRUmaPyxYLN8nDlTvPYqvDvToQRVyy5PEqEjbqdscSYIZL2zsLg+FIF8scV+UmdxPX1hHKnjvt10dYtaRUsX2/IT9vPiZHy44Vo4CUDBbEiP+9zJB5hUQ9xbyGkT7H+RuaZeXmjLA9BYrOWBMxcUYS1bXxJkEpAUbl/DwZb5thQtDeUeWXrIBvTBHzeQI8nEDPyl0dw8UWO7tkBagy2T1R2v0SzB3aTzsfMccyYxHbu+OSnYEaiqPESBhjK9HJWlLdSaQJl5QnMM0YsfNc7jMszqIvH77Hr7olvimviqsZIkqoIlKM9H5cOEgCWTGeYQTZP1+vRFOvvRczj+Ow0B4ffb6PLTx7upac+JtEOo+nzjI2EEDR0IWh+aDPdt6IkG8Dux71tKwR3Z8Sdixh/IJoCKOnCYMI0UMiJuqJ3KGdg2CQBF/H1/FOz7/LoADBnJg0NDAVoRjDy/k4UUjrDuL3AstyNn393A0btyV6gKx3WBOwaLZKp+zF0Cn5/Dh0m26F27fnv+szMGXGxal/TotTycODMiSNB5CwpA2I9piJCaR6NjZEZ7Zfcp/Yo0lKRFq09gGgI6tfvkbmtVX2jnfMWjvM2IY6Y3GL+fj8DWSbm2zDHA458+aqfHZ+1kdLndpN/soOoe4odYmEQa1v0ZwcTMNQ4BTQ4MrwM6hNmTRraQyOr1yLbCIQJ5S6pFMsDQ44F31mecG0Z/5lf2xeQbhzF2jVM9pmZ4iGWI+H0gwnULMowuvHhqwlzqUBiXL+YcPq+dwqFis0WwvJOPf/pJCqSOjCfdgzHZwOq/4WRf8HYR0L4U8/FMXBnpd3kUQKH91X+KSeZB2+/9fuPv4WB0u90rTUMz756HsIgr3wSg71GlmBnTbHJ0OQzY/k6Q7JJFH3UYkhVWM+g+4OSZrYCtGbzwZyVkQLvjTSdbTWwKsPAbQ3pImhZgqrbOwJ7pGLsO6ikD4CO+xctGJaPpoowkbI+AfCqBZWeE6wL7TYMj7w8B8qzjkN9sryCCOSQUV8vkXOYngRytu+27z98VZFOKvDblqQnbDr3qAjazNiOySAnhHq7mIMQ91p193oxl2pjG3lCHurfSgNd193beWuY3/eTRX2ZqbtQ1T5eKollaho+112VIgM045lxFAbKYrV8HoWOxkIEu465aeh6bOuDbJ3H2rQ6KazA9FgMedBGily0V5ojAVp4Pji0qxaytzmhHrVmqlqBNTXB8g44fmW1e9cWKieXtyngZYc954+M/O4KHglBRKivj7h8/6i9wuI+VZg3f9yC+ON7hrbGafG+YKTOjDovKLdoyCeOz9pIjWSFl9pme44jmF/++ouV7KaUbkeQCZztNp4/M9qcw0CN3qKzcSRnrA8K36T7gu8uj3gqC3LSnJE0bYvtYXebVTomPFSoJNL1LYVX7clmAMjvF4CBy1vG5RcEbRZIUtp6OjMOXzGW92pM+DpAop/y7IcF1g2U5VAia2/e59ZCNPQlGKZlQntIg4EZzj3YJt1oDBIyLNy9xcgDWDGvwk62EYT31w2ZN1NzhQZPjLfUIb58UqPNt5Hl4ABQ0Xq1RMDx6wyuySAUjRB40w0XD4tuDCJAgZIsTJppJMxQG3pylYqd7p9FAVxUTbsuFlXMmjdw483FWmgUQctTOEMOm/LWTNQVpkKu8DFB1SemJ38uPW8V3mzycgf7fNX5GOy31lAPM67fmWNs50d9TvNj1VYmRZmO0faFVe3AVdXpWiE8oxxZWYUvbeqwtXPVOfEs0nekInE05YzDGLZkiv2S1YHkreLwTqnPzdiSQsDyQXD8SQmUwiXD6nFCPaRhTmt0ut2lPQMQOrfqQpE3SqsA1Z79gMo0a9kRqQCfs6POpJOSAIt0BImu4GvqUaIoPV1mhsDG1okNay8RkEPGdp+Nwcixdxy+Lph/fN4p5dTXM56+n3H5jj0Tl9sixj1Dma7xcFRn8PEHWY2av9yAux8T0rnq/bhKO6CtPja91nRROLYtGevbSUsgtm9nfj5vI/XbcRBrxDSSIT4VPvvfSQs6W2IVV92MpWUhrCRGOWgCvB4B5qZq5yCI6H/9+/fwS6+Nod5TiBGK2GzecL2fgGQb89Igk2hRqS+mpvRqzRPso6j9zdxu2De/G03f/xZ0VTIjI4Pa8sgertKhm914D/98lqA2pGF808g8HN9/e+rBYKlxsveM32cMNO/yurtuv2bbYEgQ+ne7z0mHbFwpnJD2jM7AIntk+gx6jghdIw5yI1wBLr2xYESQtZMDPIKJZPf4Pod0/HXbfGIchvv2MY8mgYDmYg0GahODJ20P4yw5NaoS0ZDrtzlzT8jHEAEBhpJ37ZDkbT5ix2C0MQaAqFFzpQnrCDseHLp+sjNeMVZFYnIpWaLD6EEQIHp2TQ5vv6jaTf3v7iQRQxl1LznBDcry88jpU3jXCLeHwOvwRmcCo1/Xru1NXFt3ZHquUaP4scWM0P4+RWwujDWKuzV4Mx6y//uze/Y1Zc685zZlmHM/6/i8jRTfdGsdj0+x1gBl8jH16Mkf2O1gD1j8GIHU+wlPv2NBWQjTWZWcqfVGaW0iPP4i4fyDBjkUfPlwxm9eX+FUZlxLUphPALGC27YItgcyqKf3/IkHSQBWIFkUdfoe4/QLd5AEnH5nA3/vAgAoHyfwk7ECP4o2AfRGbCnt8mif3DzHseUJmFVtXGpD/nhFQC7WjsCNNTVTX75WyJJAdeqFjhbqt5lQDu7d9U0fMNx8ELSEscC4WAK+oLP6GEOkNRiNgbkV1PcqSKt+djpZ0WrrDgFg0I8lfrU1B54tTGf3BVSXCQ0Jcr9ErjCgDof2/Kd5pLu85zyhHbQQUgtmC6ZMOPyUQZWRrqpSnq5Nu/4abJZP1jgxE+anNkCWEsr4+SLIjzrH0lmvXRX1X4BgCaZwogSRfLb2HQKcvj8jfTmpwro5HPlsUZ0paYSunDXCJGikLcmKok3xIj2tuPsheuO/YIIijI43+oz8nvUvq8cpOryWY1IYazjymZE/ZnsO0I4GZN+7NrQp6TiacU1nrfkbjatPxGhRMqh4CytEOEbw41EWQjXSilDG8sNkEGRFOq2dEGG5KNq0mFYJCkNk7VqVZKxWK4sQcEDGvPVoPOZnaWF0gjzhetJm1FomlFdLkELAwHanDOGYC8OcqIcMfnUXCACgEdvdj6uJEfe1MT3W0Lz0awAAVaYHolSHnKKv81Buyw8+cXzWRirsxgtQ3886pAlo8olxw3zzjTsSioORaoJ6l3H6Bc0NzB8AIYWErq+VYVUX4PR7Nvzgl3+KJamQ7E8uDziXCduWdZ8TPb9kQZ0F5djpmuVeugROeMPd87p+Kbh+twKTYHlzwXdfP6E0xm+e3iKtqus3nSqmD2vQcTH2uLHBU2/7xkiNxn2eIIdFPcNSwR/PuvFPOXJylSaFdkoDP67g0wWyaAVq21Js+CRAOSbNl5BDPhJe+4tiomK5I998jEmn0aq9xY2Vkw6GhDdIIw7vvJrPDfms2L02tNN5o4agGX1XtR33tWMIAeCg1jJpU8XYYHuTvrFNwq6Zpt9WUri4HifNj5n6RHoClnc5oMV87m1DaNVNYDq5kgKi47LPS6o6ZdOlU9PzRY10EC5Sn1Nu9CULZBbAmG75pN7u5Ut1sTWHY3DnKtqiwdqzABYxeS1VgRqmEOqtOgZbQX48w1s7wCA5mRLaIe9yTIFKmJpCvVO1BKVSU+TVYpomwmHq0X6opxQ17N5pdvq4RZ+qnRNhmpw6PmQKFTTkhylyhDvykC+ZGVjfKGyaLhRND6lU4LyqI+2MTwZoSpCS4LVoOogU+W4kihbtJAJC62j9Vl9O4boDClgkbU6L9xRLhHKfw1CoIbLO1X4JPh9I+2ml+yWcDDJW8fKT6z5SHKLk24M8ZWDz0+8zVD5eQlpeOD5rIxXHN0RNzw4jTwTMN27cY1ThD+KFSK1N2hl3eyVRyEZVUO5N6XcR0NIwcUPiBoagGczXGlkk5eeH5iEmnSxKfgDa5FAFqX5XguZZMCS6TctvrQmbR2g+J7JiyEGT9luLZL0tGG/ZYfdP6wa6XPtrTgH2CKEKhLUDsuYxZPjPNqpqG8Go0CE9pxPwg9GThXvBI7XeRiCgB//F/2mbmI9fbFi++XrAI/39qnwB9dSrNsfzwdrBJWz5xinpwvSWF5YLi/c/mxR2LXCNtqasrsaQnPafcU9T0PMX/qc65N8qoiV57wysOShptN8YvD4s2Ib9T6GjaDnNgDItzwkTAB51/foFuafdHQplpnFAa2IF4AGbvxBtfPLwaN6liUyuh4Fduw1nz3EF5EbbUO+RepnFgBA4+UOji9YLlGV85uasCmJMnUyj96v1WLe35NCX09vlanVNrM9c1RgMCk4URmovxYa+xwx7TdwfLPryWjW/v5v3R5RiWoHBzByIR/17sHs+oa5hEDLbHInrY6h6gEVyP5Ox6vdg9YpKIknarDKRqo+sAtleWEcvHP9DGKmflUPakSYSA8m0pZIzsmK1ISBAon2E5ZOaCKfvZpT/6yN+93e/xnmbcFonpZZXRisJRIJXc0EVQisZlRuIBNeaVPl39ZkDgAVtadjeAOUBaAcBvVmRcsV2moGvM3gjlKMoq4uglNorAythLQf8+HEGGoEfk9LZk0KC2/0B3mVWqcxOHfaVbZfRJHIch59sWP7TO2VKNQGdrVniKEVkbeWJtQiUrlk95nUzA1VBl6IEBVuUQlqjczyXfbEzQZUBknqN6Vy00SAAWSatQbPNS0kQtIPqYpOyzVTlloDkzCFSskAVwvwRyB+vmuAnT14TmsFJjdXbdDo0b4LDV1vcf5u0LUQ6bc8gF2/C53ponGzjn7sGj/cW8kab6cmMsUOoDpte1RjlcwkxUDpvQcLgtRpDjFWFmwwmPtoYJQTc0xJBjmpg0kW0OBh9E6KmkUltrK3rVy++1VzObT5Gm+RNN/RrQXra1GYlMiPf4ScApt4x9zKNYGFSyOSU+6wNIpkwP1ZM7824rA2TSW81a3GuMKHCnmkVtGNG/YU3aoROXapKmwZq99/08aryWyZ35XNAZn0+XBqmD5sahZpBdynyTS2bM9UGg2/R/fSkZAxlumkLe3yhhfleQymZYs7yqn3LlPDRQK4GPpJE7Nmqwkd39kZ9xWeMQ4uAHFKP5yPuoJij2YwV2bozxEXZwGkzctG1dmaqR8xb1bVzc61tzgpdA+FMgQjlLqPcpyEnpt85vy9YvhaUsm/E+qnjszdSL9ZD+bELS9UAISXQlPXfY/1FeFYNoNRVJ4aJEAWNbwj/t9/zv+KPf/l/R7XZUEH4D5ffgf/w9D1c6oSvr3f4eFUqlQBIJCg1QQqDNoczfBMXtKwxd7or+PLtI+7nFT9Mr3H9mEFFo7N2aMqGuzL4wupdXQDVJtGJxqYxtr7V6+QC5BMpNsy6Xzos5kxCl12iCrQ0Yf5qVqiibhCj9dI8AVZYC+9inCza2Cxi8xYaxrSiWiEpQRaoQTtvuoHUpgboMIfGoTe148cV/P5Rx+3uADrOSi+n2YpjAS+GvD1GXN030WrU/HjP0wV0WRE4eU4Qfg05ZrSJsT0kbEfd+A5fFUwfrmhTUuM1szIyr6U36bOjPSx6jkQgVp2/yKHZpsIlRcEpr6ZXyAQsE4TV2HARCKuyhTPjou6mqYYeXWwjvV/UeLPmT4o1zHTBUa81a0nFXqcniZbhIapK2kxTGKq4YAoMaR0YhZOqFXgOsNzv4XEuEhGqb/wtsYqqxsBbC5URlrNn6fmaeiDrYqwRaH7igA45KPgGuyVGM7UMfc6M9noGXytysYjecp9cjGV4ugCXq8LYmHV9ZzOoTKBrRTpvcT0jC0/c6RDqUbr9N50Fy0831bZk09mbWanWRzYqu3Y6BoD5iYBHaAcEJ8/cHEHM2ZGIzLAM7Nfbw7UAd0aqGXlk7ZEYGcurK+KrcZpOLUgl0VLEj9p0Ho5GkqzrgJcDVFirFBXf3e4ZzkKlBuRLw/RO28dQ/TkxUt94/KyK5hs2kA76c6LE7jBMdeGCO7rGRNmQsPCGiRoKNdcZ0I9AJySRqohzVHrbIQgyRV0Z53WCCGHbUtQuKNOMAuKVJDtyEDUYnCiRw/JCuqgT8n9T/4wbt3TRMF+ptDewp6sqRHK2feuQX8fZ/jey3aK/jkdE1HMbAT86fVtbs3MiiDTQ0psQDiNgsJ1DaQ737Be5/hygnqEIN+qySH93gU3KPSrzXAluMXjqi56r5kI6PGNv8SS9CFzgVwtudDy9bsbraIRJe5RZnQWJt35gy192Y0FmWIQ9MjN0zpmLrUM5Ia1kr/MG7f+zGiM0lMnt8m08jHr5nJXpEN8Lh7AiE5F/cNhweEaOIsZ33UJRw8/4t53HIdPObrQ1PQpGD/mQlzodxJF0YCJf4nNhfKtDfILQpQsxVvuOTuLxOSGhduE1eq7kEbqOhtK4NiKX1p0yGua053hfWn5sc2RkzvngDo5bGyBBh35VCEAihxfalABCrsxJP8N9gjQSros6WUgEmiQix2DsmrFX1Rerdbvpvfep47M2UuIPwCMmn4y3PaZuP9QE4KY/vUYn566IPlLSLQ8TBArWWqbX+YzvpKc47SYJr/iiPaJa1hwICZgESy5YUkGzFUgbafAz2fUWQn5M4BVoc8LjOeExCficMD1ZPdVKEOumWY+Cdl/3npQA7ZKQ2FokrKpP15Lq+7VZYhOFAOlqjds2TZQf3jWka8P0fuveEhl7MpF2o10m8yb7orrFxXdsSPKNXo2RJqxVsoYsKgARZJlARrYIxXARjXi2olFYaQEjSVowCgoDiHqQ3aZCRkUfd8HxWkU9PpnU6xWLOlqirr1WKzCIztaFgTcL6MEgPN+cqoQyCJfWYZE6bGBmnCGa+4Pdv9f3tPsj5MsDyh2r8oMAbC2/m+mzRd7BoiXf8NLHDfM7lcu5fGfCNSnkI00gXnRrJQldu05JEcvXGnWlC7C8b3sWo0Gs2vjONzO7HTPa5EZsHF5RYyFL6ooH3n8IiNxPKJkzgeqknzElkiiCFtkbiuE7nA2YTtoA0NupyEOCK6QrRGfzDIjvG3OD0sSaZermW44p1P7He+Oqvaq4agG1S3QJk0LGSbsEaFINkV9N16rEm5Fo0Jo2vlw3jP3RaCOkKuAz27zr+b9n7NzhNSEABle33B0TWD7W+5rVRSW4FEVR1mu6Kks5P5WQXcNW9pEUWQ0a0BVIMmN7rRqJ3vZjVDXR69eiemEgZwavM6bEKOWFh/rC8VkbqRePbzJQ49E6g6vX99B+4/XJMP6edMI8pCvuoqIV2KhhoopkKudsBgoAMjUc0oZrctwWgBdVQaOedFb6b8sAb0khjw1IF3vfpp8R7sQMyv1epTkxg8F2bl4BWYC6COqDsrH4yvB2D7wq82350HD8zSv4soEvpcN2fs/sFfNJW8C3hk8K047HwDbSMLFFzyY4YcM3o5eU10sBtv4eLhNkyeBtAm/7mi8nRoy1K4q7u9zSzXWNRAwTo+3SR/ZZJ4qEOKo+g3JMg+HRH+linW4tQqRriYjpGdFCBNgKxKVzNtucpgxXMkgM8MZhlOpBC1JdZktIN5f5gzbK5HNBelohWTu6rg8c1+eECYWDBW2Ye1wBvohF0Q35pOoGDql6VOebjDptPo4eLTj85I4JQgKoZVb2ZxOFLr1WxyNKZmBCb2dhRkHIowb7rhsjqEWz9txLUwj2vIJyQrubreB3H1VLSto8bJyb9jwIep/lqM+gLhyRQNc4tPGEhApIkHzY2bk9b0oenTRBOhXkdyed/1lblai6xBbrrY8r6fvMkNLY08wKxoM9KtIZgaxt25uzXN3fdmjPojMtR/DnL0DRvHU+KdxJW+3XNYxVsDIjh6qwdj2aso6XfpBHjGqkXXuyTXqufGSQ5J/TYl4maG+C3wLbzw/iDgf4MRioaN1gwo4NWpzrRxNCBWNrCWvLSqSwhGcDoYn2k9KnB4DVe/UYXJlcts6LxucqAIm++VoYL/Z5IqBVUh3ARgrx+cLmAebTi4gNO67aFmCdCdurCWlmZGbwupnh6OOhBXlD2B80fdl7deMxGng/mHquxt6jRkLHn0rqCvSmVL9zHIKhp5tFnMY93puvc62zeNlrUSKi5v4Zh0Vah0bG4lwaN027Ft6aRTxavxKQnm/GI3vvNur0Or+cw0ulprCdw268iVL2d+MKuC4eVd2kowB2zFUIgqnVddTs77Q/1xg5NbExGTZ5rgZVV9vYLDoV0o2Yb+SeHOqKWp8qSFV6E8xSIkpCa1oLVLRzrxJ5DNoM4beb+WWORkgZjXCb1fFhmBMyM9rdBKqpP5+bnOF48EC8edaSA2bwr0aA2P8FIIkaoIBfk8JiNBYmA2q0nTJ/M0eeNfW8WWeaJxvmp0eeDQYZohehA9E4EWZAHO717wrmIQ3rfHdrDJkzxrovj1bTJmi+X9k68u7QXijcakcofm6KeXVN9ocqvrEiIWR9xuPmdyLqn2Has/madEiACJiyssDmHDUn2zDKKxinNuNjWXAqM562GadV4YXMDYkatppASVAPzXBzCVggrVrb4k3uxL24bOkq1oJMMNCWhpQ1WquXCXROtnlT1FPVWWwS6UTjzQxZQ6epTwBEvZztLoNaxt2PM+6um9Z3FOwnbDCmtA4oPDoPKEdYK/pAqSco4yL04mlvepYS6sOiDMCk9OMwTp4TiwWoXvNu4ooYi8qkmNzjHaKIyCctg9J9bZCcei7KbteTvGJ4uyQ27H608BZ5fFy19UAdei9ZXdAzAzsa7cQg0uS9HBdd/FNCulQsogZK1e8bqmQtYPViZ9sxebUCaldWn9JOiZ+aJvVdP83FYd3rj3Xg6vRQKR2adCMJOnZBEC609Ubb54FEehF00rlb573xp6qbJJWGdDWFbS+6bQxixvRYcHinEUy6CurCIGsj0rs7+zM3B6H4uOuYSmY060Ts+WMQUMGQV7q48rnqc7NaL2VcYjBaWvA7VReJxrOanrQ21be7brppW5uexBwMxHqXsd3rnCSZImc2SmIlYHD4+uveFFPnYQOgRjeulS0ynGBkIWeNijXyJGRTafcSgnKn+xtvotqf6E4LFYt4JgY33tdQuvMxZZS3B9SJd/lKZTXqgvNon5pqIqaLfY+rtgOdFPJz0T7+5gij88IRKhPf8NlvJAP4hjVrWFuFUXeRFGOThLUmXEvGWhNqZdD/zt6/xNq2ZFehaOsRMcaYc661f+fkyZOZvvnsBMwFLJ6MZGRdJxWeLLmAJVMBWXLBUADJFCxjCbAlWwYLbNkFZLliZBestPhI6CEsKoYCBfR4WFhcCd5DvhfzHuZn+2Sez/6steacY4yI6LfQPxFjrrXzHHPzXnQy75C29t5rzTk+MSKi9956662TdOUtrJ6TGhu2rrrUbQSLuKch63pJQFFD1hSyGYiMYNSpSqDFBFIVAoEYtxLbWFDRCKaPEvScdSBktc/pFLDvc3Md/u20640x4u6E3dib16fr68HDFmcMqKMs7JCrdKo1CEmleGiVolBoXsNatpPfA4E6OQDDxfvaKUAWNiUGqsZhxuA0G2qGrapB8l497Tyeiyks3WsvmH5ksCbzPYaoH0E97hSV6ZhgHXFJqcmm5kAptOt34xwKtxYV6qH30aQl6dNJuu7mQ2eQ/TPskRYA7xJrhtqiQctDeTsNywcVNVgqjsogjxyMDWide6kG8TmK9EayrrE2F8JSkI61lRckar6KPbrdt91brhsSC4egQqx0UeitAsTmI5xso61AZt+UHexQRqDMGXISg7++uSAcZ9B5lpY/0+BRUqhC2qhj9HIJEf2VejmPZi0Ct+7CBtsV3kLu9s4tv2QQn/Z2ImJAIz+v2+JWxEysdP1Ro6hFjFfLbzZJKO7Qk3vEsyhwaB10PXS/N0coRMuHMdKpIJ4aZV2c7oB8iKgjfWTA66vKSL3u6IsCUSvude79ckfHYoEyXoZb4J+/93txrCPeHl7h68f3EKniZT4gc3QYkHQCVSasJaJUMVrVjAdTg+EsyQk4fmtUceuCygYR2ncBGHwoE4C9hiPOkhR3ttrFfDNcurHZ2s83Rw/PueG5WDz3JP0VqoBGW6uOt3X9JQIyQJqEZWiUErE5Nw9R+vFUBsUoJAZ9byFru/MOcqDMiKi6Seo+EDSKtM1qiqgBkh/JxTfYoMw4Kiz9CrMZKuqYh7oR2N/KykOKbVwuE83TCN6JtBQZNbqy0PSNsq3qAj5purE1lh0VbFarydvUKW2FkaPAZsNJ7iWdKuIxgwfdHPQclksJWiezYdxR+788b4ukwlIdXqS1NKiNtBZoEMJB2RFc/iYAaRaCTpg15xUjYPJGfYRZFebuZasqNJfVDU/neEieJYGRRNDVoiLTwtQ5TkWM0ab7rL0n3ahdtRtwNXvuolMfk55f3ztrXXTgUltQ1tzS2rn3zp9LGvXO3GV+/CKqEYezSr7KHDrIuhiOUkjeK5JQkVY0l8bFo817ODnBG73a+w2a6khm1GRdmK6jM1Oj7GlhYYS5OY1UGcgihlwXaiSpDzm+KozUl4ue7v1fISObGBIxdLOjn4xKGPC8jGpRXf12xX/6tf8B/78nn8ajz9zg//HZf49n6YjfOj8V6aMim2/UiVwq4bgOyEW8ZwxVFxkBhSTBuipOvIM0lNvzxvA4XKV/106xwsVlp4owFdQ1ILw7CiMwKLPPNh7StZ2kOHhblCkJch1UHwOY8mQ/qSxfxOxNEt0DMxp47c5VWQs6ky8AstwQgLAWcKbmkUdCPQxYFaLxCMPgIlXcrrvUdOUUWqhKhLDiS5ixBLBeDwglCdyjLMYwZww3AqfAqNdqlDZqFtmKS3VzrtpFVWEsWiQENhVwEKE+3mP+xF6JDhnhJDBOOK3AWQpL65SQr5LmtqpvLCisHWpVsuliTddEWB+P6JUoxDAVhXkY6WZGOC6ohxHzswH5oIy+l0KS6CExeXX27trEaHUzykyzKHYVzTYmeVd1klqy9VHEcm0JevGsp5eM/bsV6eVZoJ/9oPVbLOOmUUzIDOaKnKJrxMVZu81adGvvQzfPmgLKXq5t9x4KowaBL+sgayudq7Y1aaK4lisS4dmKsC7iO+4Tyl4NHrUIrK2V2KBo0jVQq5BfEL1YNs5ilIZjltog09mzyEwRGgCNjBPUKPRGwg5jRxIJ65SagUIA4jFjUhafFBHrOUrpIHd7sT2UuXXAkGLbVx3uC6ijRIVUIA0YIXqcqzbitNYrIbN0356jk2aCOml0I2Of/686qQ85zKvvj4fgQM2L9Gyi8bZi/07CcJNwM17hnbceo06Euzwi1+AGxNh9zKTENsV6g2JGWjsBtkhKNkhpKscOlwBmnNiNFXojpRFWGAum3YolJHBgz09R3c5zN3ZRp6Z6pWIcoHky7jZ5bPogbZK8pYMlbEydlAD52+SSzIN0qZR2PqH5d7kHoBWRQqAQKowQCHEuYkxsPLRw1mqTQgUqgpBLQhBpKb1WHQO4Ctzjw1HaxhvW0uCj1HoA9W3Aw6rsRE00c7C5cRmCSGFnPijdlxOGzDKfZgWLVY2AE7nKPdDeh4vXXpI29D0K9EIIVJ0YIzChyACF0wo6LwJJkVCBrddPOhdUh1r1lBZJ9ZfpDbPCTmRQW+7gqiQ5jaKt7DmKjmVNQDlBxnVZARolbzMEUdTIoatrY4/czfmQItF6H37S3yFIF+e8DwpHSk4PQ4vIgpFtjGhh+cMYW75JZZMQArBvkdxDRbMeDQEwKJw7p0smGxosuVZhfJYi0U/HkoNFbKUZXjGAr4mi7N82Zt1YIVdELX+oU/KmkjCikZJJPDLUPeay7pCD5AnlRNWNZtVIKTA8pyQCxRIxiwoIulq/zhGuGoWfxEGk+n8ZKTkugc8uzJcaoNjaUfSbr3kySheF4tIcCGFlTC+EiZQPA/7nw/8N027FEAtSFAmkXAKKGpIhVAyxIhAwpyIsmEoid5IDTDZk095atfzC3FOoZcMsDBRSPGsJoEU25LpELJFRcwAmxvLEDJ1BWSTsqSKbNlthr0VprJ7vbgCtk4yBDhstWZLdHvpryB/oPtzXj7WNZz/hjVhhiy63yEg8W1GZsDyVUcHj0hXJhub199qCgGxoMYs3WWpSj9I2KCjs1i38XMU42f+Be9BL1YZ/gRiUA0K3cbhu4QMRvUR+Em14NFBZ5tJuBI8JZRKIzIoow5ydyo4s/Y7S7Yo4b50qUxcALFdUmiEjg6l0Q+yMrRTwVonqYkAowTcuV6xQg+wEikBeHEy6pkTFfQAHQr4eUfaqdkCaV2VxKmwDXJ6OAD1BmQLWR0JAGm4rdmsFlrxhVLpWoL6LOrUCao90VdfQtCLjAkULdKMk9fj7AuVcxWCsReBjmw/GwNM+cTV2xA+286L9jCAR0G5scxxQeawkG/ogRsvUHfr5ZLVP9v77c1CpiMwbYeK+juoyx8lDRDlITpNylYgF8PIAFPjzWc2dERnKGJxMY8QgDiTrJ8AdEqAZQtcs5DbZWukA3AkuI6HsI2pmjMtlSiDg9cnq7fHxNlIqCrk5+oJel2doH2KFpsgK+sZBacCtQt0hvkCuaOztKQIh3WVc/1YFp4DpVcTx3QPqBLz6dEX6zBHDUHTPJITAiEPGflhRakCeAkJg5BywrlYQKnBEXASPrwPAuwI6RpE0WrHZfPKqhXAEKfQtsiALBFoEMXCdUR5LxMYWUZ0j0jEKi3Ak5IHdgEmLa6DshY5uSU7zJtNR6nA2Y1khHlFXgNsgwOoGrce3TVOtP48x4ShFlMc7rNeD9g1SKKqKYxDvVtmgCuv70XHZeJgSDYXjIhv84z047VBVkNXqVkjrtahya+5mkZFtOASY4nmZAuokxj2sQfgg5nkzv0aBQskAd1J4HZZuAxmTb1D5IM0A01miwXin6vXK8EIpGE7LdnMKhPJkj+XpJHm3pfr74WlQBQClPfMg1zNUqADpLiO+PAkjTjdmjgFVu7pSFe/fCSdKYjEDxUQo+wH5Sr6bDwFZpZPkWSHiLaXRqO/ejjh+MqKM0tCTE7D/UsD4MiCeLsaP9buksOYhKcOyOuoQTc0cECZkNW/euuGq8S6NlUhadE3nWZiB5pAGAj86oFzL89cxaO5F3ondi+WnaiKUw4hg9U6qc8fTgHwQia0eNqbOiRI5JrnHfDUg76MY4kFq0+LM2K8Scdj3v9xRDglnbUoZ56rq+Saoq++rI9XUUddWFCktge8YQHR9Ssk56pqVbUQchT7St2Vv49d1IUAE8h6YaxCJrVMQrUpzcmMAf00YKT0eYvVx7yk/cAh5IrRoyggSgBsoS/IbA8bqUKhUpKN6XxZlT4T1mpBzROgkj5ilqDdpfirFisIFzCQ1nOql9bUsUAYfAM9XycmgGwAhDPJvk0wSRXM1SAEIU0EaxViWHMGFwKtFbW2CuXcYRGaptR+PykqSRVhzRJhDI1EAoMBAVexaPfQHjx5GvUeyoBZZZZnwFkHVRL4ommF5+BIbIVSFlSgX0DqKB60SMJuE++b7oiQgL+IBfMc8xSgL3sdPDZSzDC/nWxWWXoOsGlnCcgY1NWgElSX3pxR5gYry5d3IPNyN6hRQyxWxtt2ARsmxRUfyQ3h0RMsqtVmAQ06UGUjyt9dfdcQBy9EgQHMUwYt9q+cz9X1UqHq2bFxlZ3kqoOxlro0THh7v/jxqHGRsw6b7q49/lVqtirCJeDZ1QOb9G3uuK1ZtDDstYu72T48qNcroIwseAkyFhYv+zGBiY0Va9Ny9O2+SGYV1x0EZgOostue9/97vDVMQ5mIx6HcViS2TRtt81iIdz0fJewggIXQNorjOiWCDwP13L95Pu6+L35M5DCrKu8n164c/Innt422kqiYIPSfy5T0OAJLQtiLKvg7KhB67nzuV1gvwFAoxbIy5yc4TQEqM8LYTxAhBDNQQCiJVnMIAsp73mUBL0BbycAMSZkI9RTE6Cv0BbYKVPaPs5dnDOSBacJIYGCsoVsShIKWCWgOqoUAE1IHViwJoBYJFbqMIm+Yrxt2nIuI5SmsNbVURz5qwfQgXf91ieo2DAKCNvZ6HcgECI55WDLHbvNUjJeYmTgq4ooG35vYWFwq9dewoqwetU0B5JNeM1wnhE3uvfwpmPHsjaJuRkjCcJWberW+QGjWGtjtay3qjqQMaBWirg7pL3tTP3i23r6sha2Ksm4aJ0M2yVqS7DESSa5jMVCBvgy55H5GTSmfG/gOtnSkVPA6AKzQEh6g4ABGtYV1NIrArULeQD0DAetXkdaxPlSiiwB2MTSFsaM9IGS5Am68SwCIFQasUyBJHNMXxxnIMuUG+bsSrEDs4C12fOLpahrdpr/IeSwgIg7ZP8fxPByuvFTGL7BHPikCYjJFHhtQiFXOc1FGhuSDdrkKmsfwaq/M5JJ9TVErLB+v4xLkiAqqC33LmTBdrDhCI05zmzgC31IEqcSiMXQGBqL2kojZoV/ee4IXREKWQ3ogosmJ1hPaHSYvPz9sUv2lCWilDHQQSpiJrHLmCX5cmuDg+1kZKYGu+b5z6PNSlte6jp9DUzjlFWbT9oRAIKz5tQppswpIsSfR4FtiRvCRAjBNpBDWljOthRq4BxzhiUWFFWgPiiSTvpHCRSOaT3GcVo8KqVm6bZL6q4EeaHK2DSCcB4IExHBaEwBiGghQqcmVpD0IkeasRyCywYlwIqNJksQQGkuSxboJ4Y8MtYXzBiCshHQMGG/T+BdQOjrg0Sh/WjNIMiVXdoyC8OmKYV2fu1TH52IvUDVqdid0GEUIpzjZrOYZO2JSA9Srg/NQ2L91YKzDcShdcX4CeG6quAm5QTIiy8bT8Rm6EEa+5IsAdkYJwk5shzkWZjQeQGt0+L9jmXnDokZjBl8XBgUBrQXolvTdozqA1yzMPEfk6qvdqCvmiaj69PzuUx5PoMS5PRu+i3MRM9QkKo04R8zPJIYUcfXMqIzmxxTY6JmC9lr5qdh6ZqJ0jxmj5IwaWR6JmPtxlpFezbK5LRDhbHYGdq6sLQzsvwAhZHAGOhLCkFqX2rTIGAlNAnBNilNKGXuoHLMXRbW4Dm27W9q7suXJ1B8X+DvOKMK8tWuqajdZdknFQ2SxKXXEvw8sFSOvfnAJuh7bBcchMnTFaFbZWApboOdYNHB2KGigi0ZnUgmMqbY30sGKZHli7BNd+DIsyLgFpxKnKLb3KhDt2LA0W6zBqvy3pJMDlAYTggeNjbaQAfHkD9brDoqY+kjIWTH/0lM2HwuwPCVfdgew8IWP+scIhbeFy89oqoZMFbAbKPNEIb22xgacCI2qhr6muW1Tn9PPAHcRGvmnYuXioMqGSwIp16OCHbrF+KBRh9NzXDs7FudxDLKJlFwIoEoL2ouIxCgQBtAStPr4ly82jvcdCNCw+Si7O6tBMhDNkQlltkXabbgngWjdRDj80T7pn4u65nLFmG4u1OIFsVr0KR7tG9zOL4quoMmxUUOySpoHYETfYjHps5wYBoUgvrPaOgsNTVqBph3vZSlGuqYmEWnRkTK6maafXjuJctfej80zr9qwuy8VMImmpBW2Zlg8Nsbd7v1h/BjkrHGgUbwqiJF9CAJNCgUHmhBeu9oooapyo1vssT58daJ/Rf/vRsWApWgG/jjUFsfwrlPHavUfWMoKua/A9NOLieS0HtyGUMnsE1xT3ASKRnkLgNpcAhzHlnHaSL7O3MZwh2etXmrq+sSsBOB0dUAefRFqLE4FX+hpRnLC6id+NVl+QKIpiF84axHdxsHnjnqvSn6cgcA4Bp08MkgyegPUxgwthmQelRzOIRHHixWmPUgl3pwl5iahLRCgN+7Ymc8FrpqgVcQJdUpIBBBRtTkNZ4booY1Gy9heqAUSMUgLKGsBLFHjRaa0dXs2QAsqqHUD1WcueMYNU+ToirDshMMzSeM8opVh1EhpTsiNOUKlNTdkIKpb/s83c4DmgORAaEZgnnA9RISnd1HSzE+hKDbt5tIB7oXVM0tdn0uZ+Son2yMWmgNG8Aac1lwlSHNpdsyZgvU7Iu6hjMfo8bEXFqm7NUjzqOTtmeYkxgHcJ5SD3ZvqMZAt4J8SRspe/wypSUJ776skrmrhvEl/CRHVDNahxUbYvzdbEUe+VSEkHSi+eJGKUBoo7EEsEWhU2y3slJnTLhRgIgaVjbtLNysg6ff4TvUMkf0IGhpsitV3HVe6vqFSS1mDdW5exzRXW8gOxnNyMO9n6EecvmiUJEknwmEAWrU/pngGIxxW0St8ub0jJUAmqlldE0BzZWaPkIaFOQ1NKMYOagrdWiRYJViluTkcpBrfSgY3jTNuBZo7YiDB3qILtF64EAsBFoc3huXC4Pfer/7Z3s4GeAY/GTcGCCSo9JbnGbBE1KyXQP68vXBEQv7/eyH3I8fE2UsDvzkBREOWCnnLe0zovF4R6XJeSKCKxIgvk+FbEq99bUfdVyA45oGagX8XL3YgbYincnSNoIanXczgBykAqiEuQFhuD/CwoLMKmQkGEMAPpTqCvvGOUPcu11ShRlXw1ABGgPUfQKg37NsrS6CKRCnj7eZ2k+cBYr63eSrqVxhUYbgKGG4k0aC5OMmFramdeJjOgMBRyBoZB8ixe9GsTODS3mqhBXdqnhhMhX0kzQpBuulGM+XQjhopXatFKB5XxGJD3kljOO9L6HXuR7U+v+AGC6BRrfqZPCpdRYUf1HsOaYBJKtsjTXcEAOJOP9FlZDRWniLIfsD6KnqRvaiNS3FunhPVxEmr6whjugm8+VrTbF2sGIsk5WN0Wyb6dd4S8N1IGvNOyvYcAIN2qUsQ+SVGstuaoT1oUVlXher0mLI9l4oSZHLaLgUBWYwe9VhFYz7pCmzI3bHkEfYfPZ+mau+ameahzgS42VEwjOERnI7J2dGagRQfc5pazKQupPp0YLmFXQth1V9GjuFA0ojllhGUVx2MSZ0E6VWvjP9PyHKI4EEeBc+s0ojweUYaAuAp8B2ZxdkYhHrkCeq2Sn+kcpbYwqRljOyqBEoDSRX02Tj6PeUNDR5JoTiJj2sxly+la/nBDGOk+Z8dwhJeB5EOUkoMI5H1A3rV35DWNC2M4SiduU6UIZkALt03qQ46Pv5HSw9h8X06fz49e8fyet/Kaf987RxfdJHY2HlbbgO3GyP9PlRDO2k8KeBDPYLpYyNSdq7u2HwFAFNIDmMA5SKLVYMVCMqkr0Ov2bQQi7X7NgFl7C0uIE3sS2tpW+BE1gurYkF6sDGwjVPuMyQj1BcJ2G84io40X3S+se/kbxj2vzGA5Z1EZM08LpV3rUALTDZsLIIet7jGauvdTocbF4Dp79UayMZXqqmxAh5mbzl4vbOtGMrR779lYiDIQFj2Yhw9AISmph7KeWBtKsP0B2ty3nF03pvYcNWon2W5cNlAeiRHiLlIiZjBbbVLz7F1PUg1+Y7OikRIMEr30rjuyiM8Lu2czxj2sy5BIzMhN9sgFYOKGbOmm3Te7JBaflwgesYq8V3cEe989sYo2v69JCChGW79k2Pk70OcTkgRt4HG+MNBybtY8WTcB/QvN4ez7vbl02KVD/tBB7W+LnPtj06iVDPq9fy4h7pGXixDpvhzUEGsU9bURSXEFKG7kjy6lkADAZI8oEGiaQIe9LNIhiUep0JKxx1gZgL5J9JMh2GKVyT3cMQ6/FVFTbJtdb1hsMdpCtgRzBMpOFzwLhX15nDA/CTh/0iIYyZcYa8ogvXpVkK5XEEnhLq8avdwlpFNqEUJ/z4DXYxnk523VA4Revop3nI6SQyg7gXeoAMMtsPugqPL3Kp5vJJTrCfmtg7AcT1k08SxHYuNmGn27CfXxXhbwnBFu57aRGJPNGEvM0mIdEJJAZgxjcA/cig+H24ygyXRPqBNp4bXkIAzKK3vC/IkCHiooB9AqGyoHaXpoGnVWlxYXSMK5PzpDH7I0iyNtK+EKBQDKFEE1IMQgeTVmYD9IBBQDyi5KdAK5ZmL2ZDTlilAZk3r+m3bmkxRu2jw1I1f2EfRoBCfC8ZMJp0+EjVdcBsLyeED45BO5/iFpUz9taGh5p1H19hK80WFbR6r3qFGV1NjJXBlupXEmINeyAtGygysQmHGPZ2B8JazY8UYjnZ2OzX7E5iAS2HdQEgW18zgVPggLtEbSeVg84qTT6nAy6efrYfRaPKpCKOkVRYRYokazVtC8in8QCeVqAj/e+bUBNZyDvpNpQNnZfGKEcwCxRDbOZgsAHyZ9h0EjCzTnpbJ0ZDbEwYyKqcFbxDyqU8iM4a6CAzDcZYGaawVPg0s+sY6PQaBUGAEV8SyISF+nJYQYmQsWkYOU8ccDwqIMYTVQw7FivNXlkTQX6qQybexZo0ezZIot/z3Zfb/1W7+Fv/JX/gp+5Vd+BcfjEb/v9/0+/OIv/iK+5Vu+BQDAzPixH/sx/MIv/AJevHiBz3/+8/i5n/s5fOM3fuPv6jpc+YKe+fqHJsuZjAN4P7lHb32MTIOtpzUD2Boo+39XuDbeiiS9vESrdO+uy/DWzAC8HqaMwPJUoBgzCEsNmJ8SlrdWjE9mcJXcUs8DIGJ8+o1X+L+/+dsIYPwvL9/Gb33wBOucEL804PAOtcQ0i/ijdObVc2i0xEnaefStQsCEdEcYb+Q510ckhtE2offO0hTx7gw6zeD9hPXT17h7OyHNjKvfYaTjAq9SV/YfJ1FV5/2I9fGIMgaMr0gKbotV0ps33KBAmosqRxDiXecx29jWCjqvwmrrIznSBW61P+oY5B0wvHXCblpxnges54RahAGW90Eg16MV1QL0Sin4vZ1ieD+lqGrSlFnrQRqkUnbiGUSrMWL2+hlprif/FsNYEZfquSdjKNJylM3mMCE/26uSdXDat0+Kfnom4Ph2wPktuWl7njoAy6MAqjuUMWB+HFB28FoWS8A77DiItNGWRQmUHQtyoHOZVMljvKk4/NcjTJQXkCLTu09NouOX2L3zdGIcvpQx3Ga4PmJXUNwuKHOjHITqDkCYl6av6DJiAgeXUYq+RwBxBjhX6Y2mzSVJx6seRqyPBdYcXxUMt7PncIz9hjXLJkokHaJzAfYjytMdlsdJHJRTcbYaTxLFll1C3gW9l9DGL1cEFRLgMaFcyYIMi6qtEIFVgd7UPezLxi4Oq8DaBDGYpmwBAMONPGM4r5535DG1tiFT0NonaVUi+S8gFtF5lO7LESFKyUHZEfIB3kDR9jeqwWnl9ifdVYyv5JrL0wHLI/G+TJaLiRCqyEZJ5Fz8z0c5vuJG6vnz5/j85z+PP/7H/zh+5Vd+BW+99Rb+/b//93j27Jl/5qd/+qfxsz/7s/jCF76Az33uc/jRH/1RfMd3fAd+/dd/Hbvd7nd/0S+Xl6KgYp9ClCDbwLq/fWMz2MM3FjiTeHO5HmJgFgIEyWe9DqRIzsISiCYOamwaqoS8ACEZFKEGLAGI7DVOwlZrOwUFaUd/FWdpTR8zYqzIHQGj1U20ieTKAV2OxeGNoonuKpFWnMUwubx/hdSu5OqCp67ZZzUgChe6p6cGagPhvC68vySwXMIRxpbqz+UMLCVwoKtls9ope082FnY7kER/GCo4sEvgNGhNb6G/DYdT2tjKfdi9KcKySfht79+S1JvmjAopWTJZ5hdJ8L9h/zV4qx+3/p4NpnMDpnCbyUHVKJuVFN42h6lqiYPAM2iRu8K9/n7tnNr2W2rozAmDyjK15w+RtIEjCTExKURaoL2gLLkfvI7PE/sd3b5eRKlGTqUCWR8KPZn6do1SnOvq8D1132A6/bzDcWajuN6fsz6ft++1ETPsHsIWwic4E5XQrmFzwd5h+yFvIL8Otd8iI/1hqE23LtrvyD/j89MWQH+wXkj/vgdPXqwNJgLBDFWn4HJ5fX8OaXbpe2kF7hU4f5njK26kfuqnfgqf/exn8Yu/+Iv+s8997nP+b2bGz/zMz+BHfuRH8F3f9V0AgF/6pV/C22+/jV/+5V/Gd3/3d3/0i7EmWj7soNDIEil5Yn2DL3vhn6gqSzLxAu4AfOIBsvDLJNFQTRJ5LI9lQuzeA3bvay+XCORBvLz9uyvGlwvKLiGd1cvUzSAfJMICQYpwVd+vf5VUCDfzhP90fEOaHtaAq90CIuA8TdIcj7t8BMGZiHWUYt06VsS7iN37pOG+eqhVIsPpeUEo0nRuPQR9nhV0zs37iRFUKqb3z5JMXavU7MyLbLaltHC+o+Smm4QwarLZvO4UXX7KN+TXvUpm8Tw7tQBOURLZuxHQthdsqhU67gAwvgTm3zngZlcRHq24vj6j1IDjyxHDK41qFmjbb8ALLatAGpTZYRGXTtKEuryb7QZj7C1JjjeWmDlAptcYFlF2B4A6RmCMAAbfLOoYkHdCFiBmUTsBUEeVawoCF2cllsQzcPhtfZ4TI501IhmB4yE48++hw0gldVAl/giERedJlRYwXvy5aF3MAgw3uUFkutmGwth/kTC9EALM8lgkkYYja8EuewGxwbKbEoLNmgNMWYGjzNnhWBHPFaa2XiZDMiJCDhjGgHjSfvGD1tylgHyVnHzDSUSAibqopmj9W1Gx2Y6lGk8ZA0k0He9Wga5MZWIQzb9QGFBCSR0lwgrIrhVojRltDsRVr7EWhLMQDaymSWNSMQy1bmjv1vrCc06A3IsWiYsBZqAAKWuTOncqO8NtKQob7iowNhMhpNbSJyzqxK7sKAIUCfCaM3OAPQ+s11AZKNL37qLTH+H4ihupf/SP/hG+4zu+A3/qT/0p/LN/9s/wdV/3dfgLf+Ev4M/9uT8HAPjN3/xNvPPOO/j2b/92/86TJ0/wrd/6rfjVX/3VB43UPM+Y56aY++rVq492M9aCw2nnmn+K0Q2U/F4XrUZQxtzznjaX69lgEYUa1itCHYHzm4z1ExmohHhKOHxRq62TbBBUgPH5GeE//g7i4QDwJ5CejsiHgNObauxU0sZFaJm2a5aA0zLgnbvHiFoP9WiSqOq4uxYjhy7fBfOktT7occZwWJDrHsNtxPS8ImTTDWSMLzOG946ySK2tO+CK134zMQg76f0bxA/UC6uKM5cCXtdmpMyoEyGOA8ISZXHJD7Vv1ACTkvFHfcDRYmtdYeoKHROw7pM2miNXTpDnZ8TKGF8F7L8YUHYB57HijbePWEvEsV5juNXI125ZISUOYrDi3PoyrVdJxHkt2iCp+veW453HGucibTmIQPvB55VcA616fyliXHdRu9oaIwobxyiepRUHFZbNdhRLWkcgX8t5xxeM6aUYiXSqCHNF2UfcfF3E/EyN1xneyNDH1qJHanAfJ/ZIm7L0hBpvuRWNZvl7uF1bqYFFh2sGnZVNeJgQzwfkfZQoSluv1ATPjcVFILENqSe05+cIrHsxRjJuJLV8KstUJvh6oyrrdtwlyQftEvK16Nvl69hFkdQkgOYsclqlm+tmqEjmdzhlJFYo67gAywqeRtQnO59zAknKHKiDOFGUGTivsteYYYY4ACazZdC1dwnvFUf077Y2WEQFWCdphBNBYLn1GJS0oh2crch4TNK/TMf3fqdmdUCIUZVJaM8V19ZMMR0LrEFn+zLcUInzL/dSTQbMqfmdwf2Q4wF+2f++4z/8h//g+aV/8k/+Cb7v+74P3//9348vfOELAIB33nkHAPD2229vvvf222/77y6Pn/zJn8STJ0/8z2c/+9nf/Y31xbtA8yK6wxllHto2imb/x2pmnFabFBapAJ2DSBWdgXQWj0T+AGmWPAuUkt3XCsjGCq3hUUPFhLIE1DmirkGNFlBKwFqDN1JsD4ANVmwQlzyLCn1q7ZZcAC6FYpIqHMxoJG2drn8GUeTgaRSDMg6SLLaFnLvI6fLYKI6XToG6wmtJupoSKHHFv9PDLwH+Hr3Wqstj+HhewnWAE1goA1gDTuuA05qaxhm3cxjLzTXODIZCD3HomDsUiA1k9xCN1yG4C9hlw+IjdZAGnXM9Fbmbl4AaOoXz7PzmdIi8TqNVR/WE2x/bTNu5+3uWsSJYV2cpfIaLIduGFfLroRuDgqwI1/UpFbGwPJ21nK+TRF11bILOTql3mK3Bp+5g2nTxjRcKEwYnXpjM1gZuBRqrVVmPrkKREjAMrQ+a1aSZBmOvfN97ktz9IfLI1ddT6pAOrbXq2XcbBZfXQOV9Ibg8Q2csur3N5qoTmV53TurG0ccZrTBY94qgUZTJLvlauPisP0OAK/DDafCvR0oeOr7ikVStFd/yLd+Cn/iJnwAA/JE/8kfwb//tv8Xf+lt/C9/7vd/733TOH/7hH8YP/uAP+v9fvXolhoq7neXyIGq5qJRAKcmks7YbBtd4op02sIJtEHmnVNzukG6m8v31SiRgOALTc8Kj/yh1Tocvrdi9cyeJ7yGCY0BYCsLzV6jLAux3ft08EeYnUgxcDhVhUJbUKSG9NyCsUrNUH2dpVrhG3J4nyUkNGYdhxVoCwpkw3ME3KoNGyl7kkOoomHPJEbRqvkA70paBAK0lmt+49s3XhrdPWFsTvJAr4gd3oJs7/VDwhS51aAEwjL8yeFlBt8f7DkKpCFrfVKcBdS/TkubiDD8jtghxRWSsHCrUa/ZKBTV13qEuPOmLJD8bXkR8MT4DGBjvtlRwZoHjysQoo2x26Rx8M6fCiMaQsi6vQMdmhGyGIISONUqlIp2KOz7i2HCXBKcmwZQafNVHumUU5hggNSvjyyz5zDgga6fb4Y4xvVhbnqsyhrXiioDppT2o/JV3hPlp8Doo66RLBRhuNHqbgXSUqG+4Y4yviuskCpGkqW07ZGsCqR0d2hwRJojyN4Cyi1iv2/U5yLtP5wZryhqU9Wn5L4ty10NTg9hEhiTzf3k2IlwlJQYEn+vmeHhrlFU7Ol/tlErf7tsiD9Pms8JqaDNKWNdgltyzyaZ5CQcI67WwKaEkhrxrxfoWjZBB5HbtXJoTdllbGIWsISzX6vlAVqMs41HdOG2ilsIIKPJZwHUO6yh/2/jYeghZxnO4Y+y/tAhjj7lrnaL3lKW5o3cnAFB30lGhqmKMsSQRBCX6KMdX3Eh9+tOfxh/6Q39o87M/+Af/IP7BP/gHAIBPfepTAIAvfvGL+PSnP+2f+eIXv4hv/uZvfvCc0zRhmqbf3Y1QkD+BBOZL0mabVV/twUp29VBNK68mYYS5urMetbCTIspODAgIuPot4I3/5YRwXBFujsDLW4Brc9pKRZ1ncG65HROQzdeM/KQAQ0W0eoQ5YHpOiCdgfoMwHwI4VJQcMJ8HUBCF9RyFZBFWQjrqxKniBdcRyFey4SLJplILCVSiUE0djIYs/WWs+FNgHk2Q2uZQGWmWjpthqdjfzKIhBt33NLwnhUiYSaWAAOQMXhf5t+UHIQvI4EVKARwG/zlmbVERmyvNpKrVITR2JrX2EpXIE8UtOiA33gAwvCKgSvuHdGwOisn7gIBqFG9i5CkI48/6WpmYbanNT9Joi9GiH07iQRsEFuaizNEoqjgMN068idg1z6LiBXGRd8cJ3hIjZEK6W4HKyFcRixngY0W6WbbtQwKcDszKQuNIoMcJyyMZKEcGArSTLSTnsLD2IQOGU8Vws24KT72flh2mYci8ia6NyGM6elUNx3owqSqB7AApeZgoqGHuIFKFGM2g5l1HerFLKURYE7BcW78sdIa4qY0LtFVE8HSSXlD9wUE7AmgLi2EtoLMy/2ozKFSqUKwhxpwNfrN6NZJ2JsaSK4NCxKuwAKmIQyOKFmL4vJFibLJN5PcVPBKNRpmvFaAB1dQfNMe1UeIAYIrtRLzJxzekqL8QHOFIJ0Z6fkK4O4mTaI4i4M5JmAuSoVGAtoyBq8UI3f13B+B9xY3U5z//efy7f/fvNj/7jd/4DXz91389ACFRfOpTn8I//af/1I3Sq1ev8C//5b/E933f933F7oMCgaxV+ZBAKWrRaXjQQAFo0E2BFk7aybbwkeHjgMJz+t2axEMUxG4vdRla0yDU1gJ6/lLEQkk2JWdaDQxMBWGoSENBjBVLEsyeXPbIwhvz9hm5Bsw5YclREtyztjFgqaCvhVBvJJ/AAyFnAg/VCRMGUYUMgRMJiFGLEA0eY8udmGdlOmNqED23Z+4X+b/J21azbF5R+3d1n8GQRNxX69V8g4PBBiw5rP69dexK+3+rb4PAH9R9jlqOyUDuXq7HVR9ssyMbm+08aIlh9QjRPNxeZsoKOQE4ucKhPCLdiKzFhtSUMMsmIa/Z8ly07TYLYc0Jg7QzFKZOAnibDZPvkblnUJjo20nUGbT/UGiFuloHFRbJf4XS4D0hSzQ2p2sTPkh2eGCNGfSUZX0Gln5mcYHW7rHXU7k+nMJMfc2XrTmD3cGSzPd+UtYR1v5PDf6z9xqtRxsBZT+Is2GM3n6a6frwBos2b4mAIow+L19JncHs5tXl3Olzf7KeanMoLP/F7A613Ieu+y6P7vVzHeRn86/Bn0pZ79ELZmfckc5b1rlu+pWCsBhUqY9QWQgQ4wBXmSFrUCl5M1Ferw6PU1BNRpPXYVkPNI33e7C95viKG6m/+Bf/Ir7t274NP/ETP4E//af/NH7t134NP//zP4+f//mflwclwg/8wA/gr//1v45v/MZvdAr6Zz7zGfzJP/kn//ffABFAQYp2r6/Eox8lhyJJ+rTNxOnLE69GN4qoYamSK6rKvVRLlhNco6yn6C5PCC9/7ygJ5nmHOD8CiLAeSGp07oA3/s0E3N6BxgHrdcL8WGqjyhsrHj87YkwFV+OCIRb8lxyw3ETEMyFfMTBWhKTMriqu5KkGnE8j8inhyXvA9W8vClU11pSrp08B52fiRacjY7zT5mgrY7gxT73r4+SbEXwztXYGLoS5Zo+I0C0q/zuHZnCuDqhPrlDHJHmpRRh+9dEO+dHYWG/MkjeyfJd66mSSP0MCx6jwi3qKuwHlahCmFuuGrvcLVYBeHkfUUdlKGonYUQdZpNEo/Gg+gYn9GnQjXiF37CXF6s+aSM7aGRjwxna+cVneoUj7dqrKEjtnhW92Mn+VOs5ByA/ji0XIFVNqbTOW6oY5ngv2z+V9ja9Wb5i3gYqiTAau0vlVziG6eVWjubLrmgUuFkXX1tm2Vwwo5vHzxmB6ZAu0KCMXaRuh4xBPOqZjQLoT2KrsJariQAL3nXQzXQhRDc96FbAe7Dp6rjOwe7dgfLGgjhHroyQ6jYPA15b/MURkuGVML4qgCCPh7rN7gAXGHJ/P9wg7YSmbAuK6173EbMOorUwSaZ5XxghduUGf13TBaNYmhS/O7sy6UrpC286EMyOqpC+OQXJ3UeuxdKxRtXDYrjXElhAJkHOW0kRs9V6JrF4PsLIZL1LvnKL12Q70ZBLHykoOztlr0YKxbwGfd9J3S1IeTEC5GpCvR+T8QH3PA8dX3Ej90T/6R/EP/+E/xA//8A/jx3/8x/G5z30OP/MzP4Pv+Z7v8c/85b/8l3F3d4c//+f/PF68eIE/9sf+GP7xP/7H/201Uv1hBkqjKNpNQJSkpxunyypuO3QjtklWYd4dYNBCTTbZWZhPBokpXbnsgZOqScdVOupyAJbH0l5jfBnw6L8cMGhEl3cB+UAoe2C8WvCJ6ztMUdp6JKp4ebXDu1d7YQPtKihyS/OopE9dxVWkY8T4ijG8f5QISttchKxV92tGPUwYP33AeqWFq2fxDuNSRS2ia6N9b2gLOyNPCvHYF4UTF8xbM9adblS0EpjEA8tP9ihTQDwXxDvB+fP1iPnZAA7aruBctaEiGkavkCHFKPUoQ5SaniLqAhikZ5K0Ya/eM8lkd4SOO8k79F5P+uo1IGJSG9JH1NzlQIhE/opJnJYuz5HOFWEhN7CmflFs43ePU84ZczP24biAjmdwmRAOo6sVRI2chmNGfDWD5gW0H4E6tXekm2VcK+iVbGrxbpFNg1nnPQvsjQCGws9VjciZkZ6LI8BDQj00JQaBkCAOghldFf0FABcYtnlwQWvm2gyVbYiebF+UEp0Cwkk2sLLIy+GgyfnVxEwFFquJsDwSQdPeiQoLY3i1YvidF+DDDqArkHqW65XS1lMr7xhuRdQ2LAXnT044PxOjGlcGfYDGPEUzElQhklNGKOrYwGbca1JtujO3YOzSQFH7mdeLHZUBebVT7UsGgqw5tmtXjUQifH3VLo8EHWf/jkV76XItV1Fh167ETmQxR0Odn3S3gtay6Uhep4h8kGePc5XUfmHERTtIG7wbaLMXUJC8HocAniLWwyh5ufWjmZ//QxQnvvM7vxPf+Z3f+drfExF+/Md/HD/+4z/+f8Tl9SKaqzCmmEZFr2OXSLIUgKr0BkBlb2SRkOHiDAQQmFg9GjiOzisjVnKYwUNlg+gCUMcAOuzBu/F+DRaE0ZdrQCWSVvDtBhEig4hlojojCaLdNwTkA5Afi6HPV0muVRnpOCAsFXkfsTyKyDtdkIUkx0mtRYJspg9ANdbcDzqOiVSXS6Gr19TdPMSi9CPIePcwyIe1P2kDhQYxdVAIRwJzEKQralIZmhS3U18iU8ZIKo355klthUA2rL3Em7zTBg60f3KDfYzp18NNQTUVKwJoP0iUuEuisH51MTEoIcwjQlLV7p0mzLuCSJdIql1tihFLrOTiAai7KdB38Gq9iIzMCAEAIqwuZyOIGoLsJiGgjuIwCBkgel6KrD1Fd+7LImUx5J0BBvv8IRbjlY66vkyCB1DCgObZDELNrI6imOZgxbsdzCVK9npJltwJEbxmyzX01DhY/RuINGfZ/lyySVvxNhyxMBZdU9ynbY7GjEZ/OFNO85vKiDQY3p6fOLm822XPNWeuFoBMWYAVPYpWd6GfMdmiXBukS1JPZuxmqgQaoyBQ50aVf3C928+M3Vf5fvT9ZY6Pt3bf5aEq5wgEJI2ektGnNT91iZ0D4oFk8X5s8XMU1WlAYIK8IyyPyPWsBg3bz59g1LcWUKrIL0ZQCQiZtJV0w3OhZefLo4jpM2+hXE8oY6M+cyVUJqw14m4Vq7eW6F9FAGKSbrulBNSiDK9UkVLBPAw4fuoK6bSXko9nIodEBUinAXGG1qTIfaUj4fAlSYaGpWojtrXlix4YIxlj8iiNSkW01ubq/X3o0eduSKEYI6wESO7NDnUsPK93WVdhdOAiRqhqS446EGhSodPMXii7UU8wb7Y2kgAV2QCHk3j+RhSxwzqMrgqJ9Tk7V/QgqcYH0D2nXDtPIpdjje5sL87XAgeWXcDpTfH++2M4ihRSnKUBpjDd1Js9C9wmVG4I0WFMCEMCgkgAeSuKy7nPAJWqwyyOHPX5Ebv9whupHYNtmIN/lq17dAzI1yPKLiAsFSPatPBz23tNLRrx8bD1YMy4TYIHmF4WTC+F4HN+FrBey32XKaA+2gnLc61IhUElYoyEon3RchFHwZiSUN2+vc4HUfdOWkeWEU6NWdqo3lXg3wmoUanysZvHvb0ZCMuV/G56JTAsGKDHovVJlcXQHiY3BmFpBkRO0j3+EAXSnpS9t0pURMwoh1HqsrTRozt+vV5fZVAO7Rq5INzNUu+F5iBSrZv2OqRixDSJ9JT8iQg7KVwOS0F8wZs9wsfL8rB+X4S4VoFErSD/Q46vLiMF4F5reFOdtqLUgu1ivZgM7jmopE+6Uy+lygbj+RVz+N4EpqsFu3HFi3PUxKdFUOg8RPm7TIT8eIeiCgJyD3YrhMKEosu6lG7GB0aMFTFuN+pxyNiPIjZ792iP8xvS6+r8CUZ+LHBNvIuIyxbWYpLJFheNgrw7Ll4f+QCed+IxAJng/aNsDF9nqC4jrW4i93VnfSHg5rMXGyz1UZTXzdi5WKLBAcAMWJM3TwB3zjoAbYkiNUNpNvYevMaoFxRmUhbaIDBg+ojyLuJVQwtOCXGVSIDJqObqCD0RzbTNdxOBckCaudVuEQAE9c7JC89FbZ3aexqib2o92aGX+OE1wFX8q8KBJKQAH+OOydY6VJPXy1vbDI6WWwpIkVDvUmOYsUbjHPzZHe3o1MQ3dVC272kNTjoVhKWiTBHr1YAVmtxPJIoSEGNoz5DO0puqsI6PRYkaTcW5uNNQB5IOxRUtz2PjrS1BhOFZgBJgElwmdXVZm2dEFIm4hdlJhVF2wWE2Ew1G6Rh6rztIILeyEwfaeqnJHDEGYxDDqffi6jhWLqEdDZzscG4F+n7rtqYMIQmtn1a1DsdRUiChEFgLk1HZI04ZO13f1EWbgAYE3NqJfMjxVWWkyAxUjAKfKHXT2VT3vqCLhAB0Ujf+6yryLZxZ8yVS2MienyK3PpUJlBhlx6gRSCCfb/J5dlZSyBW1Rqe5c2L0MT4pddtvJzCQCfNpwBIGlCUAi0yMeay4Gyuq0l+z1mxxZNmUFH7s654IcEhSKvUDyqMdwjRIiL+WtpnZUWsbw8KgRTX8iIDR3OjOk9K/OQbQXmpP6n7cFG9abqNGghciZ9MJ1M13Mjp6t3EllQiC1Ev5MOnCdcq5fs8iYvm+jQ9QI4OU0Wj0aMAgmD6voItMSxMcxtP3R0HgUyGTyLWEekuuLNBrKQKQ2rur2O5JNxUbA9ngWKMjQtybt45Gxy5ACp3jpBsvWw6AVOLLqMcVzRmznB+R9xySz9TOEevmQLc2qHLzGWyci1Kmh7iJLpvBCV6f6In03pkEAFXt8EJoZ5UBDJJ10UFrpM5FyJof3FuysUURUEiv1UCKc8GJUBGd+GLzh028lgVCb+SZCmPFyQMpVGawK6x5JftmLIojEJ3I1QrAK8JSMZyaA1BTAEUIPG2F7crOdL1KG0c1irGrr/I9TGnllKu3LGJltXqjQTMMo6rO9yoydmi5BAI3qnkI27mszEufU0Sq5hM2Shk2ZjI/7IW263yU46vDSClhAjECwyA1NEkYYK6EbRsqNYtuWlMgUQCug0wSUbaWBG+4XeRn54SQR1UVjzg/JXg9huaO0pSxPiMgE/hlwqjCs2Vi1ENFzVHgpdOKMESlrEtCN1m+yR9J/58quAbQEoDjBFRguiMMt7IAXb4mSvPD86eKTMxK2oqiaa2R9TWqslDrAGQQaorI+53CXaJovJE6sYJHZe2EJXt+SjDwg0xG09Tro6RpRN0l70xaJlUCjxHQzp51bAnceC6IOuYcAvjJRVjRH4FBGMFDBSIh3a6Ic2jeZIAUHq/tWWS8BDarEXodoeg7VG/eoOUS9R6tDYFHfBHIagDiTNIob84o1xPWR8MmUpYNylqDSBH4ojnOOOvGSQI9xiDvNB8YdccukxRWiTir+QQkrWKisRErw/Iq5WpUCDFKdMCav1AyiMnlcFBnYRDvXiJPHa9OpYCtBxggTkwgz82ApciVlhU8DgiHAVTC1shFknsyVW7VVjQ1DM8RzZJ4lzoqW5+s40beFZhT0FYfMrfLSKjPkjheSiaIM2N6kaWA+kgYkkU75MXEXu9Wba4YNCzNJyXnsko0CHNgxGjFU0ZYquSxMrvBMycsFMb0SsZyuMkCsxXG8GpBPBd3YvKVRoCsa60w0s0COquhyhJhMTSKnwJCLlrEm8WA2FjlisiaMzSVDzN6BuNGgYHBGr1dwrvcipl5TCiHERy7MVOYO6piic+PSqpIo5+bi9Q/QhzfqDk+HtSYfTQb9dVipBrGRGawDJLooyNL7AEtWaskgDrIhKGgkh8K99AsEj6BGUk1t+pIII6XcDliquBdlm64x9jopxFAZFRtcWAGUJK/8vveQLVnsbAHoEKIJ3IlgOmFeMJlIlWTIJz2DFxlgAE+i8im1/moigLl5tFXVQWw2hJjM6ZzAK3dfTDApUqzRmM7KczkSXJNNNNlxGqFo9rPZrOQtcgRZg9YmJXWk4o19+Wel22IWVlfsHBQPhDWKv3EkkJ70Qz0/UiqRoAH9jnQf85aa7TrNi/cCx3NWw/tM7RWNeSTt2GXTdiiHB1L6GazgxdMG4EEGklh1LkxVlQGymJtPtjzP3Ukv5f+EEMmm1Ydgr5bqB5zFyEGuV5rlVJlKZkitlGgbeyJYP20rG9bi0KbnqLLfemmK/cUhH05BHVYlIi0AkFrxjzaV/p2q0/UyFZfN5IYqlCkxsqiszrInM57mc+jzt2wqEOldWSNpaZ+pnUmKFVkywDwnprCSYVD4ZQiGOQbvEU+HEiGLBJ4tDWr12ZBB4x8RAs3Ydo4eirCQDfOjKh1ZPILdpYrB3WUiGByU2ydFLQVBstZJLI3hqbSzjkEcBSNS1TW9OAWdnO1jQqHi13jEDInnGhkKIUxfKNBo2heHzQSJdbKdH2fXzORFJGTJURANm4pkMD9PImykxwLh9UIKD3W2ggwy3e1d4MlmcPSWH9hAXIJKCVgGDIOuxm1BrwCMA8jnIG3ispD3gHLW1dYniRRkXhWUQ+Sa1pLRGFCLhGVgdNxAp0iwkxaqEutfkZtMSfFvRPEOK3iodDSmvrFhUArvDDS9LU4ApXIjTIgCz+cVY5Ix4gY6nG1vEQbex3DwrIQjEhhRwwCb5IyCXWjEa9fIRytP/JaLK2NAg2iZk4dxFMZ0Raf0ZqZgcwAC/GDxiTeYhU6OimxIs6SYwwroewYGdQYfLYf2H6thts3cW4epBcB98aB0brjAk0Xr3J7X0X+HbRYEiBlfcF79MjmKycfXwSUsxJAVH08MMHablv01ct4ybWDK7+7MbBXFsRxCagth2h7BVs9i8IzXNR4dpGxUtAb2UXeWwwqARUkMT++RGtwp/OlJmn2CF1SUBgqHfMm2pVnGr1vlutP6jyBwn5SCE8+BiBZB9byhgO8nqfvoBuWiqTnlI1cDEk4567Rpt5f6eaa5UYrSe7FIhgo4WElhESoJfj7DhYlLo0Z2x6UPde2YYZ2kCOA1o8tRnXk0KjjbCy5VmzctBdrawQaVHk9SBRqTlgdotRkveYw1CkURrU8Gve/l/PxmDSi0lwdAExDq2e0fFsUVY8yReSvGeIEBVGWiFHbcQRXlrB6jf5oE5o8P0KFkW4XYbnYzy1pHJqatsnoD9q0LmTC+RQwzxE5Vjy7OuH3PHkPA1W8+/QaL857nNYB733pMeL7osE3PyG8+voJy2PC8bMFu7fvEAJjiAVLiZjXhNNpFAbfzYDpg4CwqITRAi8utTxGGUW6SdhmBDoHyWGcSDc/QjrJd/u8lNDh5RxxJiUJAGFmpBdH0HkRSq/mm2jNYoAsiV7qpniXCoNOs8gY9a+HGWFMQGVUjs2ooa1XY0K6jtp5cQOZ90nvVYusZy1+Pc7bBV9V/48ZPI0A9qAhKORRQUTSmO9LAWUE5qekA0HeidfGxaKrqi1O5Dmgmmfi6XJVske3wdQxuvcbT5Ib8/o7ghdKsxouaX/BiGqkmCAElyAFp8MteRFq3otD4vnFKs6SRE2E5SpgvZJ7CVqj1wfnphEYNDchHWHZmV9ggMcoSXCWTTdqUSgB7vjVXVIoE80wMhCvkzduHJ6fkd69gb9kpR/nvej0SXsKVlX1gvTeLaifN0Tg+AzLEylartBSB0CMA2Ts8y5Igb06FUyam9qp83YnzxGOi2+kTECYM5LlmKxDrCEECleHJSL0xJHOOSNIZGgtYWitiKfVIxWL9NxIVMDbecCiUZlDKLxRJ5F3hUZUAUSMAHDEwgptrZBaBJ7bCRwAMDIUEeqjHcp+UKiSnRRh7MT+MDhV4OcqUWAWZiBVcY575modA8r1tHFeAWhhOCHkKnJKpxUcI9brhPVR/O9bJ/V/5mEdd71flDY3fPjDFz9X2ImUWUPnRTxDjD4y1sjMJwWgitLsSVgU8Z5SqHhjPGIKGYEYu7ji5bzH+/HaI5g6Auu10MP5kPH4cEZlEmZfJZQaUHJAXSPCLAaq73HknXU7KJEN3gJEtVojppAl10KqXC0P1J69GoTVGXKqSjdeVpnsFsZ37C5rx+E/t0VVquv4ye+UzVWFYWXJVnccuLsZUnipqhHUpLqx1uyPIxN2nX4j0R5AFIIQQMjOJw8e1op0FgOR9yIRtRkTuy3b9ALQi2AaFCVtINSjpi4QCdRYpK5awa544d6vJZxJcyGm5oBm9EKQd8gkecs6kFzOojn9W4y5kHLqSF5vZriiGzUikJFpSFl53D27wXIgZ53JerkYnNAMp0UybU7JxjbkKkLC1gnbWqtrHjAo15w0QU/zAj6e2zhr9N1+QL4rNtIKOQGm1SOiI8W0aeV5Ui1ypSwdnU0fz+d27QxRre7o3mtQ6Jux3AdZ/mctQnKq4hQhVy+qxji4zmQ/pj6nQj/PdM6qIfH+Uh7RN4bigyQX+7c9mz6HtweptUHbsSP32NcDHI4Hgvb+6j+Ai88rFFgJVmJixBUTQ26lJFDFe0L9cizi7vjYGylvaJiS/FuZKJ5XqNRYRQTV2SJPwkoXzzb5qAK8ZqEtKxbum6pF7GtBPBVQCdi9F1CmEWU34D+/N+E/P30DFBh1jsASQJUQTkYNBpanjOWpVr9Xwgcvr0BBOvGGwEI7J4CidI21JPc2YQ5YnqTsWkTEJkxbCfFMoly9AsMNS+0P4BOsjNpWfiCkk/QIiqtI9TQmUeu9RcaSZAaSNDzkGIBBoDVGBPaTjH93WIsDQCOlpUgOqVtgABp0cdZmiOY8aGF1GZsiuJ3LDaa+O2GqSX2ckTWcqQRxNMZXxWuc4ixGxiPTC5q8KW/3i9LziEHzD7oZ1iGgjJMoeSwVYZbrpmPxOi25BzuRbS7irVKVDsFlH3Tjp44QAc05oBmjAIUkJYJOM2/GxtvAd4KhVlgalH3Xd+2lrnCcITCYw1FOR5b7tjyvnEeule4K0l1WEkIFprENWq2gc8b0/op4ihsYLMwZPHZ6jsoIFXkrzTudpSwAAPI+uMrHVlfOxlJq3QAIaYEZrHViHnUwq1yUEQz8jTcSQdWFFwIQIvp2MPJ7CHGBaAMH8i5hfTyJ4vcxI92Kw4Socz5oXkrVXcJZyxgILmQr80KdNTbVjgCaC4bbjDoH6Q9VymYNyUvpym1CEAOte2Hwomp28QIqLMXuUaHTYDWhgjikMzDeSst47zJcoSo11n4FMo6BQasVCAtdnqOqvj/eIY6aC2NIzd96ce+vOT7+RioQMIxNQNYgKOuoyaoOoOyXaq0OiFq7dgT3tAA0dpMWsXkvqtDw5+HVAo4B8VxxeDd6kWUZZXFav52agNtPB5w/AdSJsb6ZMT6epc7iZkB+dwceGPl6RRzEdabAcjvBNhDZGItRkFWQVgwXu/EyrzkUVZF+Lj2F9u9npFstJNRJna8GnN4aRFPwKCyoOBfEV0vb8FXGROok6ga7ZgBeSxNJyApDvJcMNTaUJZqjYeQVTnemXCVysyjKzpFF3qhqTrCMXR2QfS43rNuosjyJKkOZont2qKKtN707AxUYP0jgUSCb8xuiobipcQkiv2Nja1Cp0c7NSTA9R2LCqYinOz0POHypiDrCXUa4PYu3fEqIU9wksgG4oeXdgOXNA5giAldnTg16/wCwPE04vSk1e8ZkowJMNwXjy+zGjrUP1fooYtWOvQKNkQu2hqybV75QjwAQli6CNoegz68zVNtPvPrp/TPiB7fyqxgEcmUG5SJ/yhnDecag86bv78bTCN6PvsFyELatGdvxZkV6fpLfv31oyuca7Vo7CatzG18WbwxJlaWMwZhwGln0G7k5Xz39XlADgxgD6m5UcojkZomkPQYXQxL0VV4NOH5K2qZMryL2XySEVd61NA7V/MyaBZWZgUAEpIj87NAiD4MeAbA6zCFLrtgltnr6uDmWkzYQJUhuvYv+LC9G6nQQZC8zhmdVB2B+RDh+hrBeM9KJML6ICIu0TxmO4mDEM2N8KSzcOtpaE4KIwau4GoTcA0IdB4AHdbi0DcvaT6jXHx97I+WFu5aD6gkTwCY0B3WECYtGFEq4xwIEdEKb8VIg1mCD0lhMYSnw+g+NyozOWqeI89MdiCWZSFPB9eGM8zLg+GpEmAMqKrgEV1sgQK/V3ZJBGQFec+VsM4uguKuJssZ3CyOeCuJx3UzoGAPCmhAiVBqmaO1TN/HNUEWVSupwwb5dtY95uIcEtPG8ZIsZns4sBmru7s/erSaFXyuf0hs182St+NCq/23cAwEowhys8px8jghTRHzURX8OJ2nU0hkp+33P9Ktjg1rNkNU7IxXoz3NxSr8//5p9E/LnjsHnVA/rOJmHgbKP94geRLJBh7Nch4YohbYpIBdLsm0j8LbB07251o+DG6j+3VD3e/XGaS2AioxiP3mRN7PW8jCLGLDBT9rmgYcEKOECSYkI/u7USVsVnkuxscnQru85Oo3Q0qkg3a0K24qDGmyeWCkKGroidHps943+vfjcDlsmnCI1Xo6h808UGaBwqDJfjVXZ5beoG1s2w8EX1ydTKIGM3cr3XpXfIyD3qfJDZKxJ+8hrmpKSOq+2ZuoAlB2j7CuIg2slVkcB4PAmFfYyHz8MXkXbMryQt0J6sdk8/wjHx95IySJomLPyuWWRVGjLDfHWOWpVtvVbsbbgs7YyvpezujBcvsGSh+sovFG8Zs/htM1VlAbYW8OvJSLn6HUzYAJFadHRH3moWzZUvzlAJ9dKwLK977BKOC2dgeumbYPl2AAgHSviQmLE7lbQknUj0ecMluMgh/vI4J4g4yPt2gM2dH83/thssNKWuwojLwSwUoUoX0xyG2/NGYQF2L2/YriR1hIm47KZA/odo+D21f/UQ4uRwAgayQnVfXyRHU7LU9AOyeTMsj7n0W/O8nLlZ+lIGF/JnJpeVYwviySdO7UFmHad5wNks65Xk49jmYLfc1iq0+1tg4lzxXgTUM42zno7UeAi0qgozDLOIUdNzBNiRQfXYEOc8EJme06GKmRvc0N1jFivgmvmEQNxCRhuB4kIYgAfJpTDCGhExElFS28XmWNrEZLNmls0HYNEPLrhxbUivBJjHWaToRfVhvFWa6xWlcCqMt+FuKGt0tfiRs8UI3xfsPemUD5H7TBtYgA2//SZG4kKooCObRdUz1uzwNPTq4p0FrULKweIq3SktsjfBVm7+R7OGdFg7qK1R3oPD+5NRmbZD1LLZMoXxlg+SjcHh5aLOAh1VDknahBfHQl5b2K8Wjc4B2cFS+QMjDcFQfdM2BomtJIDQByQroBc5qPAgAjkpSj/3Zoe/p9+VFbtthb1SJLYPDk0iiaLIcmT8PgHbVNAWsNgEKEflxNWcXmhSOtL6H5vbdbvMQoToUwVbEZqTSg5AkpyIAYoMsZhS9+ep4Ki7DaqtGHU2GYZFni3WU5C5w6rtjq4K7Jwl6z1FOqtDvKc4yuV1z+vCDdnYe9ZW2y7d8fuoQrszUC5MvLOJBDUm4zmzbWcgrR7UCxd+FotuooPUFEtitD+NOn5Kgww88wBbKSW+ojqAQ9NaMxCbCDitmGswLgWDM+lL9DydML6KEk9EgUhfkWtRVMqt1DBJa9oRIjhBnj8n7MY/rkgnAR6C6e1RRKlisdscJduzPMn9lgeC7wV5+65l3JPyy3dZUwELa4U+SHLN5UpIETJhYTTCkoBYRmUqt7GxPNmlhPQaETmLXmJgECSpW3wgVB2Ecu1wKAWLcaZMT0fkFIEp4hyPWF5LM33Ts8i8l7m4/79hHRXMNyuSMczeF6EMbbmhoQcBmXgtYZ9TqOuLCw6Fs88jU15PJ51rGr1aBlIWmtnOnnB6/uE0AIXZJW5HkFKmfaoS+stTbFdWttHz68KZEuAoF2gUjF9sHaUeEKJkiPGsnYM2eYIennLvDbIkRnWQNTnuSEH0LkcxRjkxxPuPjWiDtp7THtrDaEJ4rY9AKjjgPUqQZqctkaHed/624Us7NO4aJTJgsyMzxfE49rU0VXI2whKzgwMwaPskGWviXfSSmV5Nknu9WuGONEfXjjYGQ+FM6RWpf2+UbKwDbEvBm4jc9Ifttl0yXsyEVSt+PZ+eAoNcVQG1wP7qCAT6vHY34Eb0wwMa1lv9+33bzalkjyjeS/ePrz7msKetlD6vkCef3gINn3oCO05zThZFOUacwwhdGgtlY+xb9aX8E33mX581tazxu/to8AFjLYpXH7eXsTKoKxR8Tqg+EaGDva6uCVDyTQPJYuaxUBo7Q1dGPyHntHGqkYgaAjTFEK4CbJ2StkhaysZszFk0Z4xsqAK9QYp2XronxubOdTGSq/djZXR1/1asREypNuqbXQaGWrBexkDyiTkHmLZEMNq6ggEsPRv8xxw/+4rb1S4PXKoGhFUqbsjaxy5Fvl8LyVk3+v/7q/T/ducMERSxxYtv91/12SUbB31BkTHypr+iaZd9/0Pm68a6Wzuv5eNCvYCuBlPZe2VURypvqbPyCW0WfOdI2m1blFJQA0ZFs6IEpxsnknRNrcGpA9Zj9DlqYFGzlE6vuW1Xa7uIxwffyPFtdGh7YUWeO2DFXo2MUs4bfahvkn3Tn9poCqLB9xtGtQ19/JDN5YaZaHyoYCGijRmpFRRa8E6VZQaUCehti85IsWKw7giEuPGKMNFFqMXZOZWjU6125ggE6vRlPme5IlAnuTGmTW6pGlUzyw4fMHT0KIooCX6WX+mob5h2fY3sWykEj10RhTwHCBZJAXAYZiuXsUawK2PR4l6AYQU4UK4BvFdqqPrAg9FWmSHtWrVv+bdDEfvPFLbBKzoMp0KMkXkvajJkxJR4lFygWUnVf9hAdKdjPXuhVxHKNUF4e7cNsAhtWtd0JmpVgyvVu927PdoG29hlCc7nD45oUzkNUZgaSNfduqUVekRxisjHhWWrQp/abnEBqokOFTmhqxCmIgMT7Jzz9bsnEDPySUAE7A+ihjeegxOAae3BsxPRJW9TLrmtLbQ2lQM+8lrkppocRKGWxRLSKUKJd+if8j7pLlIVM9RSAWAQ+1WOCrwvkYomjf26LkzAl7LZDkU+3uI9/cHhRs3BASg5eRY1eiJXDZKUByd5zF6tOWeY79Rm2pHjwhwAO+0KNaIGywkpboTWnuZgjbzFCdV8lFAmrUkImtzTI28rClorz0ZwMCZEAKjZlnLtWN/6rIXncGdwKP2HupuQBkEcaljAO0TrNZuOmePqOouoeq4Xpa+fLnj42+kAHAp6imqO2T08y5R6OwlUvo3W96BPFr58Atxm1SxbW4P+kdGpEiSRI1XK2KsGMeMFCowAnmfUSkCiQEmrGtECIxdytinFe8PV+K09IV6Fjnpz2SzkO+bBx6cVnyRoNQ8kcn3s4bxdVBdtprAk8igcLdxuzel6g6yCNskM3kjq1lBVV2viyS397gJGlVxF+V1njKrN1x3A5anyRdJUrw+HBepibpgpPXN4kxMMyxSaGkq074JXB5G01XFgTJKy4z5zYp4JEzPgeklI+/IG16mI7B/ryLNVVqtL0VhoAy6O4nzdNiD99MmsgbgpBHK0pk1vRCDZYQKNlYpCcR2+3URyyMxlrsPKuIK5J3kEQQZYIQiigfDjcLWlbwBpAvk2hzo6MRhBQhyLy0S1I3bIHBvaqiOW0cs4QDMjwPiLIb0+MmI5QlaVFYBTsB60GiqBEy7EUFllOxPnRKydgcQR0defD4MyAeB2MYXC9JxkeeoACXtHjtEJymEIDleQByrwBC5qmUVw9jDbVj9/fM4gAZtztc1dzT5IbDAt1b35s0Pq8phlQpMSYgrDBn/3klMoobDyFvSgK21XOByIu7gkN6LtMYJkHXIY0LdJ9QUZE5aJBW1VKMwykmdApMqMlSIIWvTBABs31D4t2StvRta9AzAWYBWdG9rpe4G1Cl4R28mQlgZuy+dEJ/ficN5PaFOydVKLvPrX+74qjBSziKqAEKFVKnyxWS0z6p3wOo599CLJVU3m3ofbt+HDjYha2jfqaMsGvNyQmhtNqR4V7/j0IF6ZcQI+sd/3/0tlexo9Tapux/u/ui9kRpLGE78UIhN0A3RtMFUnNU05Vg723Zq2dasTesyQWBYcWsw+Z/cVdIH6ph9F+9Ex94bKPo1sFETcCZmJFDW7wT5s2kcBzTDqvOALggAGxjFoAeD1eydaLTgxE6VcurhsVA0B5C7gkfd1I3Q4cWY3D/bNoKz71lxKXWfkaJIhc0qsM6EksXZ6rUDqxYM10TeA4l7eEo/x92/7xFBzGlLAUyDIhHq4ev6kMS+SkoFeNGxtRxxmEmNlNHDXTW7f/19JOUK+XDJJYv6eojNIxxLwIeL57T5ZFC27QVmdGvVPNPl3nDpVKG909dBU4GadFQP/Tkc3SI0TgFQVjCjc5y/zD04w1ZLaLhITpWVFFKH0CITfX4KQAWhRjU0JnCg79adc0dAgA0kbE5MX/LISgDbBYBaSxQATti4Z3R6FMpqV23J2Vz4CMfH20gFHUXF/v31WpOuIQlcoYvdoJWDDl48F1VglvAeVzuNwDRiGCLKYZAJX6pI9hR+rWHKV+Lx1YFwfhqlN9AemD+Z8XQvjLTTPCCvSURoF1EOBksx7zRmjKlI80NreBjghAhjVI0vCbsPxCiv2n6+X1BUoArrEbSLoKvk9NZg9RroFonuvQQSEVCtQ6FTRZwlUZ2vBsxv7mDMrxaZilQRdEOwzTwsTTPP8fvKojK9gfmUMbWX1gHh5gzKTajUGg/GtTYl6hjBB0lw8zR6zYgTP3Szp0IeFRhxhPei6GzCmX1Jgrd3IEI+KLy0kEayLTcZMoDFIM1uPppTRCQqKIDmaRQCqkbQgdSzDFsojdaKUGpjqw5SkJz3AcsTYHlWMH+Scff7K0BAenfA4behOoBixKTn2YCye6Q/o1aU3B22Qbi2XBUW2mzK9EP7XjpVjK9UuDgQppcMjtyKmfWYH6nWnsKjIUNU2mejhqvqx7EgnBctCNe6nhSQDwnrdXCCSliiM93SUbyyOgTwk70XidZBjJFoJRocqQWwRQ2sjidf7eR12c8ujUSKXj+FKqxSAG4QiCHtgPQdN7o8edt3Y2JSqaJRp4a0RkI5KAy2qtp67WBIQ34ucmm8H7E+HiWS1G4MVBllJxJTRRuy2jvlCORB9oKZCWUcvL7P5d0yHO4rQ/cO9dL5QJjfAMqehQF9lmirTITl6QBUYLwZsXux25aHMKR1ylqkFGOK4E8+FsM4Rr9+PBeRBMsXOp+vOT7WRopUBolddaC6OnNMSTYuANa0C6Ui3czCArKkvzk/YwS0GjpkkfepU8LydESZhPo8vJJND7Wrcei83XyIOL2ZUCbg+GmSthlDxf6NEx7tZiwl4uZuh3Kb3COnSuABGIaCw7QgqqXJHMQBjOxFx2UnpIzwAWH3XIkROWABebQBkkko99NyZRyUnfMiSzddxhZy0M/V2Cr646k6/be8ucPpreSbkNSkAMOtMiSzTs5edblowr2PcgzGCAHQ1glCiRWjM+QKOp4l8qpdH55F2E+sbdbrEDcbb1gKwquTsLcAh/tcrqZUYIgoe2mhUQ4J+RDgqtIdZRbMUjAKuByV3LNeS3tdhdx52vcmJ7mGpLdzrwDVCoa0WK+72AozAamtO2uXVJVY4iEi7wLWRxV4uuITb9zif/rUb+I6zvh//q9/BPjtK+m6PMgmAgDnAViue8YlnIXobFITKTWWamXUKWB+LEn4sgPWRzI248uA/S5oawbG9LJsIhx3yh7LnJbyB5lvu+dFjNJSEe8WzwvRvMg72Y3yTlJAvoreyTbkgHRHEvCuouCBIPp/5TopGy0oGYMxvSqIBi8HiLxT0Xm4FvA0NIczi8Pz2sJzQIkDRZmd2jiSIVCi9XAKtu40yiGSgvjbLI6u5V+SNIIsCp/FlRFmMTbpLkv5h0bfbmh1Ppb9gPVRxHIVhJwzifpD3kvRrUhN6UJQY2QKNHUg0XOEOjCTvO/hRkom5Dq458Cs18D8ZgFfFdA5gm+Eil5HoOwlCh2eB6zvRoSFMdwB4231fm7pKOzGfD0gPw3dupK1k25XZUZuS0led3ysjZT2Hrj/MwoCtZh30rNrWCOnypJX6aMiNVrSsC6JnMc+aPI3gEqSTZM71pwx2wJhvQrIB9ksyo7BqQJDRVAGW2USCndnoAyes5oBBrBoFFVyRMiyGRabTCRNFc3bdM2yC9jGCzepeVG9eGrPWwDgofkmwd4NjTHQrAxNchqsTff0Q71nWru/rQj63vsTAoVBr5fJPVEIV+mV/j369/VzPZxzycay+3IPFb7BVGsbYvkZiyj1figT4qJirVnvJ+h7u4CtTLjXI0wrEO2etc0/jfRqEJi0v08/IXlJA28DLsw1YQpZGm3afVzei/7NHYzTWIOyWQSFZGUzb1GIt1Dx73QsQUukMzeojRps1PIbatz7d2qkl17Z4SKaIZsGGtVyheRUbEyszYfNaZ/nl3Oji0hMVeI1c9CvDcj6RDenQKKor/kuX789lOr3vn3HTpzQHC7Zbnvp2AS5LqF7jmBt6Tsorb99ZhEI0LnqJb6Ezdqnbow269o+EwBXwufO6bLfp4q8J9AIQXS0jIYHze2DwKe2JzboGvr8/djoXNLrPPA2Hjw+1kaKSxEv5qGjVnDOmq+Q5n+WjJZeOw8PUdlFnN4asFxJvUDWv6kAcY4tFwRsJxtBIJmnjDpU1H0FxoqQKpjF8Kw5gnMQ+Mg2FiYgB6xrxDkOyFX+XUtAfjHi8JwQZzl3uZLIKl8zjm8HUAHKBFdFCHm7MXi7c6WbUoIUi3YT8bKezltPAw6LAeKVcdJk+V7+HRapfxlvbOJGh0ZFgbltFK3Qt7ugMp8Cs7QVYXhjRZSK8PKInbYMMeaUXEhhw8II89oq3AOBd6Pfx6Vz4mPSCdaCbFPd7hxhBXYfsLD6FmB6KTBV2QVp2FhVybuDTK0WpYl66oal2n2kArgAEGtFOG5r6igLccKgvvxkQt5H5J04KvkU8Zyv8f+af684Pr+zQzoJe4+YfCNIZynkBjo4hyWyCUWeNd11NVh2/Sk4Yy+ujOFdGZvxtmJ8kRFK9VyGsfXWqyDQ8kET9iuQTozpZfXxXq8SUiiIt4Qm6to2dGnlIvcVuuJR1hqjQsGJOWUSTbnWeRnYoAIMIQXY+x4TeFCo1hRi1tIkki4O6uapRcDrIWF5LEXR6Y6QzvAxkKLiinTMGrVXV1YP5xWDjdcYRWqKVIzWjL8KxRJzywungLIfvOjWWrBQBeJZWwoVOVeNQqDhq/trGbr2RdMQCCu5A2EGqg7wflRx1neQpTVQGQLC1Yo3vu4l9sOKF6cdbl/twWtAviKgBsRFuiykY20alFaQfS6SUrGl1TkEdQior9u7L46PtZGSh750v7uNqRRBJXJxlhIbQSIAWxVh+VoZA05vBpzfFM+h7Bh10NoVE0btvRK7BQLy44L0ZEEM4iVzJb+dJUfkGsBF+kr5dwFl0QesJWJZItbjCKyE4VXEeCP1NyZNgsjIexZqtN6LsbTagoVPwr5+plajHYeOkLCd2dzNiCbZb7g6tE23tAcRmKl9X+APvU9TwbBrXM5Hhb5k0NUPrJBN2qCPu5Ow5FIEX+1RrZW8Gjdai/x+XoBhAD86eHFmS6aLgaOiOUtPtMMjFOvzJP+R5w6ZMZniwcoY7rLm2aKqQtDGIXB40aBF6q6tPbZ6I2XabfcOy0UY/HUdUSZ9v0sA5oDzywFUCbv3g+Z7bDIp9fjEGO5kbIPmpKgC6SyU9LBUDM9P0u4kRdSddMylkjQHSqBZoLqwMIZjRny1gGoVdYN9AjFJjuKxwqXavBOr6ufdrKgxYH0URV9Q9edcddwnjb7rqkW5K4s4CsNLRSw/ZlqFdeg2Xs0pX0bVlCukw29o78GiQXMGnFTT9hAvORkSpKc7oU6E9YoU1WDpF2VGSgV+w1pBJy3GtRKJOUs32kgIYwLltNmfPEfcE2o0NVF2EWUfWzdoIoCrihc3uFUMZUA+tEjKz6VLkSGOluXYNvV1ytAkkuLdoF0TKEsudtxl/E+f+k38nv27+LUXn8P/PH8WKyfwRMgElIWw/6LAnGEuovCRJE8Yj1nkznpfQA0wpwD+iBz0j7eRet3BlqPiBidcTMaHv6fel25g0kBNksScAAQGk8BAYSGH60i/SpVQcgAFQl0DkMUzOtcReYyicL72rnP3N5P0kKrCADJI0CV5GMIisqLKwH6CbbEdGhSl9yaKx51ihV6zz8n5EITWPK8OAWUnU6SXwen/BgBTQN4WDVOj6Rtrroto5CQXFw/qTaYoDM3unTiE8rqjZxJ1sO49tpQZ8o5dZNFFuy8bC3vWDhLSqCuS5hYW1u67W/YgX94PNJrli+e6PPqI071tbbmydrCPPwcrwaRFUtY8sZFcmgNj0YQz3TqFem/AqJqPQRWtKSs70p6DqBUQ99cp7do1Wvfd7k//bB0cSoWBwE6SINoK3ho8KbCXRo32HtbGJA1FiCk2Juzj2cYfEISAnH13MfyXxfsP5K1ch45ZVGNKN697yNnZft3PA/weL48G9alTqExJu1YPKYdSwStp7gxtXtge0I0ddT+7zKFaTWUobY64NmQFSg64yxNe5gNu84S8RvCsajlZEaqg8lf9etYA4FJ3k2v1yPl1WoKXx1efkbKFVIozeKznC9J2w5Foitrmqb+Sdggq4joyODHokPHs2S2uxhXvfPAY9b8eEGZ4nQEAUA2oZ1FBH8/SeFASy9y8FWXqCebOyg4E6hqwcEJdIrCSQ4JSkyPniccAnuE6c6BGBYblAWzR2kIg+X3s8OEeo750ZsogXiMHyc2t1/KBPOlzFjHSFOGdYa31fHBNMki1vTZO8z46WkPUcnkPeFIhSL2KMvZgTLdSxSsjKfrkKDdPMWqbFn2PtuHZJtUbRpMbgrTpcBjsXKVFSYXfPwJhvU6oY1BnQWDCUCqm51JbY9JFfdG0yC7dN5gAgEHIEgBkbj60SDujLPpokuNMd0EZeJIAF6o/EFeB90w+CgDqJC0/ZE7yJu8kSgSEcjUC+0GLWQWiSncZ+/dELcIiKMraiFKliZikYNMaHjpjS7UAQ5ZoZ34m7yjvCGWQtcGD0pCh61FztbRkUbwfI9IpglMztkLEaIxULy4vIoXkbVB6Z8bajBjtm7SOSokqYICG4u+AL/PagBjzoKUt2o/NmlfWpPV6c5V1Zt2HI0luqBdcVYfA+zaREDK8ZMFgbM/R6RirisSl8ZHi9AKsFeGUJVqcdmASViQVIGkTT+thJ+PWORV6UAXiSeRNTf9QDD4QZkIMAevNiP/Pe5/Bf9y9gf/6/lPQFyeMZyOlkDMK7z41OIFleJVbkbSJKLv8E0DHReu95vvj/sDx1Wek7KgMLhUUIRtdSo3iqRDUtlFYM2BWic1JDBQiY9qv+B/feBef3r3E/7v8Hrz/Xw6ua+VGqhB4lkUw3El3VUDgihplg1meAvnAF0aKwZnAtRkoK+CtA9yzj7MU2XlDPp2Uztoq3R/1hhhS5Ol6bLWd795BSl3fKf4/6kRneLIT/fkNboy6irpolaNCirq5S4GfwGDEot3nHmvnuXIgLYZk0BpAqy7gXASiCQEYIhAEbOeo/cQsR9E5HOYN+uOxETAEnw+zfCedC+JRchSkDEUek27G5srqnMkV6bxq24VOISCZbmMAo7aIyYpHjQih1friiVb/nTw8b2jIYRWB0jgHpJPdhDhQVp9mShW98vfyxk4MCInxiNoenbpibCEHCaMwlgWYhfE2voQXfpJSiR3C9DlCTl7wfH1mxFnnXCSsB3V0dgoRz7zJE27UHHKBtb6IS0UtBCO0SJeBinjK27qnwgh3Z2EJEkm7jyE5S04G0N4dAYMoWXAKCKs4UL1hezCyZVbHRaNljRK9H9latXNA9etsEISerRdsHUgul6zFR99FgMXBEsJCyzX3dXleWFyrFwqHNyZ9XqhoNm/2A5DW1I3bhW/RKOkadxGAwgirQpmniOcvr3B7mrA832H3Mvh7trXFUTqOh6xiwy+5OW4O7QbtqVW9KwDVr3UjpXJJDIBISRQpqTetG33SiWOhdSTMjwPyNVD3VYxIqoC20Hi17hCIcVoGn6yhNLICaz6YKpQNJj8nkrbj0AkaEjSKC425ZcW8VaGdLkLzjUDDfQYcbxZgm2DQoH0+Ty0XEbqci0QPlpNprLZekcMiLOLtOjb2Ww+H5Ul6FknClYGlSOJ3l5oqs25mYSakygjUsZaAlkC2zUIjrTol8NWInpTAIaDuk+RQ1uAK9C5QyqbSrgSJXQJwgChYiOyOwxPa+rzGAEoBxKSRXPK+RhtWUq4enUq+K7YiXZLcAAB4Az/5T/s30GSq+o3RNBMDibEzwoq+Axh7LorjVBV6roM6FCQ5VoNpTbYJUKmcGDoopxkbGbd2a85a1bnoigspIJjmWiCvrUpnclSAsm1yWttWFMFgQhnkZ3WKqI/36A/vs2S5YW1yWQfyXkSySQtlO8xCevCuBVZeYkr8VaO0ol6aRtYhC0WaFbYUI6mwVQ+9qcdv9H/pkaa1aAw3VhZdE+l3thyUzjGBv/+wVB2rrk2N6XxevAMrfWjsXJJyFKXCty9IgW1f1O1sVRIGs703d6Yz+57Vw3yh6DMqo5UDISYh7LDm5Fmd7Q0TMNsjtsgagPaZ66ipHSTKMYA/kszPV7GR4lKAZQEoAEMR7z1n0LxDWAbUISBfjUIb3xFOn5SCybxj5Ldn7B/NYCbUSr6f/McP3gAR4+5GjFUZZeMdbhrMUVVyaThK8SIA1EyoiRGUZRc0t+DtLIrBCeJBlamLkkjZeR7yk+bJAHarodBGbBHW+lTuJazA7n0pqgQgqgRqYPNEvumkWTYYlzdKmhZSiRqblGacLeo6v0mYn0WkI+PJzEgvZ9QpCUPy2jxi+fxwy9i/HxDn0vJhDMRTRjgtLe9RKngccP7sAcdPyhQd70R6SEoEhD4eF8a4S4inSRLQp1UgHIUq60CojwaUaXcP1ux1BvtDFEKo29TFiw6L1IxZR2cOcRslLgo9aR2UrSwCAOuYq2QeAG2TquyECp5GlEc7lMPg92jORx0hPX52DN4XIDKWJwHHTwZha3F0gxpyY3CVEahJapx2H8Ahwc1R5d7YIslcpT7ncXLDB0wKCVWkWynwTnfF803WMZcKY7yRLr2cCHkXHeaanw04v9naXBALK2z84OyQmdXYzG9OOKv+X8hSGxQysP/SingnjSvFoItzIh23gzihc/XomyBwMeWKYZV5x4Mk70HSkDCclVSVJEfHBJTD0Nqm5IrpuUKp6ngZ9MdDaI0MfXJ1aMKQlIhTMbwQPUdngJoorUXTSevmVqk9BIA6SlRaB2ClAGAAlQGuYB5FMIC19NL6zEl5SrfGFmPuibzXcFvaHDN/SoklcTXyhpRggAfUMSEwkK96aELPramNmJqjxCQyVzSqzt85q9o9SZE6Efh1feIujq9aIwVmcHYTDwCgxML0yxUUjUYrk+D0FmN9K4OmgqdPjni0m7HWgOM8otSAnANOd6M0JzwLVMJK70xnuDy+jXtYJF8AqHfEJA1kz7gHuYUVSEeZQHkHYUwpzNfnj0zZWL7Ezcvqa6YALeQF8hUjnqXPkdyIhE2mYm0wJEDgRRl27rnBykEkGu2jqtD+LhMUFiXNTUkeZb0iLE/JWYSAnG84XloLRjxBjIs1B6wViAH5EHB6S4xkHQPSUY2U0qrjTAhZ38VckOYVUAIEJzEm+RAwPyY3+laY63OiAnEk1CybU1anxVhycVZDYhj7EJUUoN1vBzFSMRhTi+TdQBhOokPXyBLOIuyVv/XZMVTUMYpSCKtBYTRHJUl+k6YCioyyj1ivo3jcoRm14VahZpKoKu+BOhPGG9oSTIB7yW7vkxaAvGsdgC2q3H3AGGdZQ9Gjkgh6Y9ccntsV6eVZ8nOHUdUkIpZHSboEd8cUgXSXEFnbcsxF6qIMKlQ0AJUQV8b0Ijgz1BolupGKBNqwd2TSUmVpApkLiAj1egIPQ4vk1WEwZQlELRreC+w5vCqImv/BFJ1d2PqqqZGBwqlaa8UGFxPkuU5LY3f6/FNIjKQWjFLULsIVMRJA0g6Do/CpcJB7KiMJ7V9JXobI9MoSwtSEkytoloh3uCsYny8yr8ZOSJfhKY9hp7lu1nKNURyeclU13cBupIAg+Slu9wFAVF1IxjeuxWUSxbGJ4uh9hOOr10iRvGAAkrOIURLXT65w/tQBdSLMjyPWK4VHBhZYj4G704h5Tcg5CB08k2huLVKbFBdCOgssN9wAw111lYf1ijTKYIdcAHlRAeqB9fpv9jvtpMskdQc1w0Nri14cNsuQzRDQBUst6tIN3DTTGrlB15IuoJ6tJ56cwheVlbmIjYGRD+tfGmUgyu/rqGSTQYstVfuvLyo0IxvPkqDvj5BFHoeIwANkw5iGTd7Cz2V6Y8q+NHqzKF5PCGMSmNAkjFbGeIuOps/b6IkVmor2/EoCcLUL/VgMcl7Vl3voMK1EdwbYxoobnGde8wNwoPUoSufgmL70mEqIM0RsFAElDqiRkY7Bi2bLCN80QhYyBXzeEIISG+4XzzKsm6s8hHojCmuFIoXkZZIJYCUHG8YjC5wW56r9jMTRoCwwLXNEWMMml2mwWVy6PGYg1Zi0dyf3Y9p/AlGJXBk4NAOrqhCcZPyD5Xe6HCmCqIlL07/kjffAAVSje2Cuq6foAWCGsi1ml/+6YKpantsQAaN8g9Ckjy4YrRypNWLsxl9yTwEhMNJZKPqmICGNHLt8Ncv4eOkJ2VqH56vTSWWpMjZ9xISQou1BzL5rFGbiwVQgjObIqFlgR4L87emBKA5ZGaTHnBeA+/kCyKBsg9Mfanb6wPFVbKREuw8AMAyglED7PW6/4Rof/IGkxAg1JolRHhWEoaDmgPzeHnWWxPr1CxJPXzczr9rXf+9eFOy+OINKRf2GKxzfNiPR1IXjyogrUJUqzBcORFiB4ViRTtbRUzyn9UAiTTMICVEmpURH8Sz3VLXAFpANKR/08QuQbknEPwMh7/TnhksbhGe5KlWQCBkYb9ijpLKjrZFEgw7KSFivGfkgxYVlItkARinw7A0rq3Gd3j+LMrK/JwKPg0gdBathEwZZnjojFeA9b8QokhYqSuQTChCvuvbi+tdwk7H/Yn6Q8mvPvT4ehcXIUktkhJd+Q89XCUDqIBo7AUAQA8SRUBFalMyQnJkrb3eGSXX5EILXUaEUxJcnhOPaCCAApjFi90HAuhCGgVBeiQ5aPIsSu5EhsjJI04mxf7/o3IpuyMQgPDAIIeicba0gwlIQzxIZrnvRoWQCxlslHNTakVKqSPsoSzDczaDzAo7atl1zdXFJqIPMn+GueM2Wt58YE/LeWjkQhqOgBWnWxpnKVCzXo85ljYBiQL5KyoIsiHeEkLcRC4aE/HgHHoIy5+Q9cWKBKSsjnbVZZbCCW4LlfMpOHdRzESKKvWIjghBAkQT6U0V0Oq/brMslOcMgPu1CYEXApPCxMBsJcQ6okXB6a8DN1wcsT6oW3Or+ctKu0JoHNAWJ8UbYdqK/V4QtajkvM8pLBbSo22tF91GiJlXYD5pvLNwkxOrInquvCaqmwaL79yzpWGnXc2Jgl4T9qPnqmgj58h295viqNVJktPOglGBtyLZcB5zfZPDQJgwnBsYKigyshHgU3bB0AnbvMoZjq7m59MKHVwXplWLN5eCbs1VyowBQ+ioZBfPSm9U6nTDLZi/Cm2hFeh25wlTQ00nhRdvIDRKKZlDRJGwIrUjXIoNqEZk8hwnMCn7NHrWI/L9+Vye/RWkmqcJK16+uXq3X7GAHG69wXEG3x/bwKgZchkm+q5FKGcM9Y96gyGboizZ6q0W9vir04LgItBbXivTy3JQcLuGuGJEPCTVGZyzFU1FIg9CaEooRso2Sa6fw7vdHG2izCf42Ft+mBqejYRtFn+alGS3dSOJ5h3QeZV6pcgAHhZRn+UzYwb3akOHRqiiThyaBdLlRVigkqZ6E5gS9zUu2Ilo467WfR/ZvWotGR1XbqEiRKuuz0ZpgWoEhM+Kpag6Pm7FR58SiOpEgE4FbE5g1CryMK6twsEQZ1lTRmXKb9yzRVpmUYKFtS5qzRuBFaeGdM9oaPFoHYPm9vWsfT5MHs1xY37eqsorX3oe3rMOtsQhlPKuXSlgBdCBCeGNA3jPq04w6B8TbKM7ZGV7jhiBrkao4vtNzcR7CUsR49vm7XoO0f6eVG0HCWIIs+0uQSg00+SqWfSlK7r6OkuuWiM2IFEqUYsl1CXz4taI4cXl0EF+4vgI9eQzEiHq9Q7maUHfSyjrOAHddbplIyA2nqAVqgBEaxtuK6WVpdGqFwBokoYsSaLRvlugongXWsMJIqkApsqH2B0fg9EYEPZX8Sk1y/aJN7apFJFqQ60QLY1MpE6dMcOKGFegBGnmM1KjqVTcsq9ivcCMcDMojSANFhSmLkTcIyHu5N46QTf02IJ5VouV6kOZ2RN7CYfOs+wF8fdi8s3q9Q340bLT0qraxZmVsy0bUqNcbeCPaBkrKxLJnV6af09J5u7miC3r0WdBBnJv+VwoxUhWWluVujDjhkBVB4CeFcMKSpOlhp87OMbT7AaS5ZNcm5l7UZ4y57NNb5om9V0gu0xLmUu7Q1MHFyRKj7f2klNkZqElMycRt9xG1uV9cA0KRbI8YiShSVhuKMZwAwPsRPFm005h7cZbC+HiuiOcs4sUKd27abUDmteV0UeFkBdFyrP4ZKoxaREOScpB6N2ZwipsNmIo2lKwSOcGUUljhr85ZQ5B6uHiScQqhOSji9MmcIv0eWRTUz7P+uHRMzGnuYd+AVk8Y4JGVOG7JzxEygFlyQJwYlSR3t16r4HE1tITdyWoQdIvkrScceg3SQY2ISjHVqPPN7K3lvgzK1wlPVda6dV9YryXKS7PBuj30J0QkZN722voyx1eHkTJMP0ZpkRAj8Ik3MH/2GfI+4vxGxOnN4IM83D5wDhN4dThJagUO7ywYf+u5sHS0zUOdEspeihXjubU1t9YNxCJlNN0Ur0OgKq0Noibae+LEeiCcP6H1U+h+ZYl2PacJm8ZF2IOuZM3i6S+PCeu1frvzBPNBjVdtLMJ0Igx31ds8tFovdsisRm2mxgyyhmZBCBll0uc8EdL7hLiI53V6U1h1VrNB1E1oApYnI1Jf7EjA8nTE+VncbATCWhI9OLBuwmRGWvB+UTYgQBlNrH+nIzR3qM4EK2FGx9S9W2ZgFLZZVTp6T433zdyMZhTF7XhcEY4rTEnDGGOWs6qjwkkAwppAc5JNbFnFS7akv7bZrleTbMKlIhwX8LqFJ6mIHE5KAZnIDVKZZM40VQK56TpAkv4FSKeC4ZUUQQsrUaOKQQpy47nKeKmxkRxJ1IgyI8xWYgB3XvIhIQ5BlOm1ONtrDCOhPNqj7KM0XLxb3Rilu4ywBsRTRnpxkvGYRpRHkzDhOsmqoE0zXcJHo6t0XhHu5s07DDEgHmXsTbUCQ5I6SYto1ox4c0YMAfRoknY0Aa7cbcbGoLcwa+0czKBLFG5lK7SyQIPG6guyD3mZwqWh6o8kTQz9e7bctUidikCGtGSpCZzE6RCSUUC6icLwnRg1sMKXULYkId1JFFOjGiqnpDdoEkEiH4K8e47SlqiOAet1FOd4UmfQnJ/BCDzcEADIOg+ztfMAzm/IfjDektDuL4RnBeIVtvVHOb46jBQgI0nBDRWmEet1wnoImJ8EzG8AHBjpJPmczVc7eKwOwBoh/coZiKcVfHMHmkbZ7DTPVbWtB2pthY7cIikqQJi1CNDqD2CeRdsEAQiT6sDITwWHdrmlTAhnhSSyGap2r0LLlT/CVIzqLcOLcM0wl9HyERIpeqOzLlkKtPPLv9lza/Z7p0PvKyiTw6J2TqnLaefC1h7L5jj1jBLF/Cdj4On9GWsp6OZrLEZ7fvP+DAI0iFUjKmcf9YfRft1QVWeQNNpu+7gL7RIc9gPEaNCyqjcsOYQaGufcjBogEaA0fNNNojc+LOrVxioLmVoks7nv9s57wk31cgXJdUq9irYFH0iinSoRhEsTKTHBtfYCtgy/ng2r+ncOHdt4p6AETIO9hNEmtUtqpCfJo4S5gFeD56TlhigmCCWZdT3ZRsp2P8zu9JlhJXuHy7o1BFmh0p6AoOPItTp5RboNV1AZ3IHzGjiNKAz2lnesCAlplBcCCkWPasjavQe5cYnyPzw68IJf/4FGoFFzslA9v6L5vM6pDaVF1DWIseChoiA4wvLlOt+6sdXn3PxOGc/OGDTptYquJpMdzpcvQaOp5hRbEXIv6utzillqrgivzRNfHh9vI0WEsNuBnjwGDQP4sEN9tAMPEcvTCcsj7eHCGj0RIcwaTXQDZ5Rtx90Hbi/Z8hhR2EE8JpSrAcsTqyHZSTO+Kt7u7j15ibuXFcMxN/waADMhngOYqpIZJFKgzNh/iVCfJ4/kOHKDFgkIWWBKqyiPK7u3GbRAeXpJiLNGPA7NqQjoxKBCAEkEVhdbaBr9FRkTaZsePA9jbLHhjjEcZU9fikArYSVM7wO7F1UjF7knmah9KwwZ5+FYMdxmhFXao5uaQ7orOFgeTFl2kuMKjU1mgZAZgSRGNx1ZySFqb0hVFpYWacJo7Zcerm4AzoQy+PAib0Xc8iOiMyfSTQA2m02Ys6jcV3Y2GK1VN9gA8ABvGaEN9vjSKBnc10FHfUFxXOHvQeDfbZ5LoGpGngJiYMQ56B4qbejzPrgRExg5AjwiXCWpBzsXb9vuz7UqcaFIO3SOQB2VEcYRpoSAKvJDZSctbsLKCqGLsfN1oM0nMSTUafDW71AKu8ke2dyUvA2hkm7iMYKpkQCasPFFYaz92xmLVfeAjHQna81q4C7fAboGldb2HFHyezURoj5Hr3biX49B9vA+alcIT+YTS42QvfoKIMKhNsqEoFqX3vJDId94BlIiESaJQZh22lKGCqQQV52Zmgj5KoJyQByCS0hxEjja9P8Itqa0gJqAMANJWaG2xuYnklKgqIZGVXHInDuC13BSFQHisk9bY6QOHwgo+aOZn4+5kQqgR49Q/4e3UA4j5jcG3L0dHac3rzNkxu59eORAHTwBSI7l9Elg/kQRfPUUWu2JilHykFAPI8ouYXmScPyE1EtwHBDWAyhXxFPB4/8o503HFeGoDexMmbsQEkkxb9kFrIeIsiOkE+PRb2Wku4J8FXF6IyqUQ1ieKvS4aLdTzTHEU9XW5VJTAWaMLxcY1bnsE8oYsDxJuPsMSX1DBcKsHtesm7P1SVJ68nIdcXwroA5KWz3KQpZ/VyAAx1PC/FSKaR/9l4L9F09wSSGTgenowcZik/oPqYep+wGcRjARpg9mhFcngBl8mBR2iACNjShhnj8pm4hJINVXVRS/O1JLT6wwOnSLdi1kbPi8kSHsMGjFiQ9KR4dqI3IkKfDpaeRZmjISS71NHLrfayQlUFIrZrXvegL/wji5RJLK61CVLshWqJtPQLlrEazlI6U9BlBWEnhIWZP5ELx7rkWpmYH5UZJE+4kxPc+S1ynsDT7DWjHcWWGtdAqQjac1bSQ1znUMWA8By7VAPvEct+K7EOeDH0kXbB6CqH94zRJALGrtpCw6noJfs44RYUgwtXGGRhvWYr0/uBlGiZSU8XgkjNkg1+496DiZ/BKVAg4RdUpYHyV3YExFIg4RbHCf7SnmgIQAlI5AUSXi5gCHfPuELUMZrTuFSW9N7URbmQSSxqu34rWGtUXqYYF3z/XeZ1Wc7vmJwr5zQDyHTSTOhUCLjIMZyDxJRCgdlUXebf9eRpwrbj47yn40yPrre6pxEpJMHdRQEbBeAeujeI9Rao5oXr8GjBTFCBoH1J20bV+vAtZHJEoQGU7RNC9/08oC3RxhJStMVTT0FpLKObP6CiGaFyWSLVBGkSRiI9TTPYm4onSerA0OiVIsG4pCIkrOMMgh3RUMz88IZdRiUqFxu1I0Q7vfAtZSnUziSYtNjcHj8jw1Ih9st5YwwRXVbX/VnBZtNmmFzmbNERWJ3oajKC4MJ8GsxcsqbmCoa2jnyy9I1T16rxYAFePNo7Xc0ELHYBHcapR9RunhEWprTXIKTeTVNjLpLkwtMumu3c7TRSE+J4SKL0xHdnjOSXjmucfthkgQ+AelNi+ajK2onw0EE9WlUl3B40MPJbY4waXIJhDJngFNCURvxnJ5Lc+jnrIViQf9g+ZQUAVG87I3hAiZc8xGHOjOkUjV+eWZra0LR43ElRAg8FWHXmj0IzV11NX28D2IyOEugjx0lHoxitzG+QHIkkkBrYtIB1rUS1QdxvOcTZ+v1LFvzQfhewIHblHx5b0CTafuyx1m1Oy/NnaVNrmqVvOFVvuo7LnKkju3XLMTigypU4WZmhk0GCynEWoPCRriYfuCEFwRVWg4zAVplhYxPSO43budgx1Gtr1yI6HUf/ZDhseOj7eR+oO/B+ujayxPB/G0CjB9IHBbOiurqbK2HpARyfuonXaFGWXUVc9h2ABCk4BvT0D4JMoUka+EFLDug9JSRf2g7CTpb71eOAbkJwk1ik6ZtRBAVJmYUWp78k7Ye1QIy5MEYOdSLMa6s1ljRaZRn8WUock8XmbA6ixYKvcDhL473ETk56lFTlUFcO8qhjtVul6s2BCgKlDmcKpIx6riocVlgcZRCiBFXWBpIquXpARmIIjB8o26ZxkZzKHep3u9uYKietG6AVOF5+WMjCLK27YZyTiYFlyY5XrxuLTNwq5hjLIYpG8QN9q95chCFumYcLbEODlzLx+Sv+94yh4psJY7bDZM8/C7pLUJ9aJWNW66gouoN2A3KuvKIkTG+GJGug1O0ZfC8aiKF4YMyDVqBKBzen4SwGkSIor1JOPGPC0DIV/BHbu8C77BkRrcsFaMt8UZdmY0+805rCIbRYWRTlFzbFDWqUB/ccltrvrwUBsfhTWNnLE+nfQ+hBEoBeaEchjl3kqEqUVIj6gHdr0h3QteN3OVYjMIPfuTRBUBmlcbdCmK4RWxZH+WEMCjFhdbrVmFNFxNUSPGpD2qqNHO3ShCGx0GLI8C0kwYbhJCvyY0oo2LiASEFdplQRANUbJh3c90uh8Z412FK+CX5pAGE7hliLQTxFEOKyMfAtZrIS4JJDoizow8EYZbbYioEl02n2T8ZC8jVcNZngA3MbqTGxfZ14YjI8wMvmg0+rrjY22knn/TY8RxB2PmxJlx/TtFOmXeLIjP71rNCQBOEfGtx1jeGFHGgLwXplTZSbjq8ia+WQE3n0k4P4nNcyRl/2n+qkzAehUQV5kwYRHl6vnpgPmJ4PXjbcVwkyX3dRVF/HWSzSEf5JznpwFlHNybjKuolwOA0eHjzFLYeKqIxxWupAzx4qkUSUYHEgh8jRhiwO6DpsBgHs34kjG9WJFenGWBz4tEYy+PmN7RVZ2Lj594vgGIAdNaMbxKwjp7cZSmg0AzAMxAzuCiifeaZMHG0GpFPN8Cpb8mUFBqca7+76YEwe4pppPmv7LlJCDq7HPReh1NzBtcZk0ILZekz2I5IWJGOlXN24jTEmdgmgvSiyO8tiQElOsR62OR9xnvKnZzaV5/CrJeOxqyiMVa+3KdiFXo/w6DaX2RyfzUnQohF93sSkH60iugiqYh70eFtvZYDwSyGh2dK5yAkiCGKhGWR9tIohWPM9brgOWJOkuVkI8EISEJJAyGbNKqYO/vjhpDkCD5uHBzBg0J4y7CeqVzFKYhiDHcCHtxA7/avPGbk3ezPH2Mu7clyjq8VzG+WjyKzVepFdezwILp5Rl0Xtp51DDwYboHA9JaQOdV5l8IPQK2ZROS6CHG44J4RDMwaqzdOYmkgspBjBfk3ti2V2YvWLcI0qPXIsWuHAPWqyh5nzNhfJEaZKxGOOTqHZfDKt2XhVpfEZV6vzwbcXozaeF1xe7dWcYttvvuEReeIuooNYLD7QoqjPNbO+Svi9INfKclB2pQxhdimOZn5Llzj5R0LYcsjtP8ZsX5bQathOl5QDoK83a4Y3WOvwbYfcs1YQjiUaOQM5nCUr1olHORjZIIlNImKSwQSQvjhS7N29/3kvm2RjfQgizEWhv8If1gJMkIBuqsvWQ0uS9QiH6X2jkEr4bXKpkUjNBS0fJpllS+yK1ZBEMV4MogkpoS8WIvNqosHhWpIgLlokWctfWA6f5NQwIPJJ8pRTBxq5Oxnk+xGSBm1nheB6eHRS7hF/1Zg94u4R7A6lhMUqdXo96cRo2SPc/lNdxARX3hOuZeSNlhGFS5a00fpHIe8Nbz1c7xIcdDzSXbL7nBhKTkgKAwlU2IlaUWL2s332QKBdsxaAWWbV7VASISbFCQwrte3OutJOBkGSFU2LPpJlS4u5D8MU1Kew7LtQgs3YkVBz1P7fKDtd5jdwkbjARbZXOM9JLmkIwtIrXGiRuCRDeuNvaiO3h/3PvPGIXcHof02cVBEsTC4cNI6Lt6Q9l6Rvdu7VoIqJLfbB2h7YFwb+56eUAULcwN6gCZCtbgMmQomiJGJ5ylA264HpTco2r1uR9nvZcsyA6hgrvmooZCuACA7ldWMG+kLQD3mKZ9NCVRpOamRskZllFgSROgJW0x81GOj7WROr8J0ELYvRBYKp1Kk2dZs9RMAZpTktxImSRfYYlmLxjV9wdtlWFMmvElY7qpG/x2PQQsykCzmh5hnTX21Pw4iI4fJD+wXKfNZgJA6hlmgSanlxXDUVlyc1GILSEuEWUgJI2iLGTfUH8vcy3GToJCJZrw7w+h9/aGQDfwqJXxBl1ETWpPo3j4QRLd4l1VBPNeeXs+gb0sF9NBffoupJKeNZ9mxlU6H/c9lbz4VCE9z6WYDp4ubM4E7xM2JNkMFDKziIqHJLUoljDXDSDMGeMqcFbIA8oi8BTNYrx5SBJJTdEln6oqPpcpKrmggE7Zowyr6xHosWyNFMMlhDzSq1U3aMDVC6wfEbO/G5mAGcRRdP7m5PVRcmI4FC3wFMAK5aWjJMPj2hiEnutSe14Hkb+RXCD5ZmQ5NH8Esx8W0RiLlWUs4klzb/oew1JBc2kOEOBOycZY6Tyans94ou9zuM2It4sUJ581ore5xm3+3FN0UAMjii/Vc2Le5iMG1P2AfD1i05aFpQbK4TySiBSAEGTUAG96QVUIzBVlTlANwNjGbdOaJnc9qMyPquZhwN9h3Q9tDegRT3J9VP0OQQzg0J49LuxU/nI1uEMrueJm+KENG6PNBetPtUqUXUfZ10QeDcIutrlC6hT15TQM2TtZ1ygFbV0kxKeyg+bUpZFm+FqIpOa3M9IHogM2vSty/+G8NE+aSJodpuiYcNlF7RaqFMkJrvAgWB7B2sOnE7D/oGD6YPF8BBMhPEkoQwQpm0r6q6hkvkZm+SD/B+DGynJBokMmStUhiyLF9DwjnQSuincLkAvSy4Tdu4MQNiYlQZhza7Rm1Pu9bCCLQxoHCs4cZ+qDBK+yl8rzttg4GiSGjbdYrkaUq8FzDNK9NSC9jP6Ze8GCSVMBbbEPqY2jRbWX1fq9wdOogAhgkNYBaQ6Rmxfc4MgKUY2WqU1rAWYxHjyJ+Gxr661RxXkV+CcGhHWHckgyl+bV80Ks9V1lkrIGi7Cr5ueoVDEeatRASfJxud6rrCfzZo0WXwqsTQrQDLJsRC3v4d/XKDHOqq9XhWgjdHSpiaumXj610orhVnTwTL1CLrbdeIxYERfyaIrM0Nfu/jtGousMGoQ1F9lYqmyCpDI/WFZfl94AkWVD7VUJGBXxvRsc3r9tc8emVN1eyyO+oPnF/vOkxbXQsVYImFOUaDQEZ+s6FKZdjIe1AkeVvLAcUmGEZZHzxAgOCRzjlvxC2NYB2o9VwonU4Iezzkl1mKi0CEvSCAFln9xo2ti7PFRsPfAqB63rFKcvndlz7PkgKYewVMS1btpjSO569f9bnizkiuFO8krCMGaB9jQfZWUCUFtvWycBTuKQMWFYBwCOQN6zFv0ywnFFKF8DRgqRna3moonqITFrXRAgC0IVeL0Q7174D08k9k0LyXrFRMGuieTnoQBsRY46gVwBweabMfi6SWwMHa8rUiMli8M2A4HQaJZ7igEATaBd9JojiTAMU69eR7SJZiqUkaX5HGzv495hXmEHhbFeq45R9PQ69QXqohEAct/x/gJFJzK6HfOLl3ABbXy5w9hYHDvGmZ2zh0ksQvQNtkMpAgksat+zyCt3tUrdfXkewZx40vYkiRA6WSOL8Jx1ZuPUJ+YDWuRUzQ3dmnmrkWnP0SLSnpVIl06KzjlXog/dv7vP+A6zGVdjrrXzgEhyisQtGlFI1yMs3m5+8Oi4Nsmli2jbn8sizw0CUBvhpX+fvH0vxGrwujos/5z9TYSNTp05TJfjfQkf92vt8rDz9qQLpjZmfa1d5XZrNgd65p/O0b5v2xbGb9/1fSsA9vKk7q0xSL3TAjqHR8/zIOypZSuXx+VnvWjeWMLAJvrzj2uA10foNh+9DjES7k2+1xwfbyNlOnsAQCT1Nc92YCKk44r4/Cjeyn5EOYy6WTHGVxV1JJShFYvGE4FDQpwJ40vCcCsqwulcxBtkoCTpNktFa5Xmhu0DkF45ewAEjK807CbV4FPIZf9Bwfgya2jcC3nWLlmunurSibHWxyj75H2Myr57dSSe2sAsSgiWS2IGpYjhtqBctI5OZ61q1zbaHEXY1lSZTa04H6ITCkyVvIeIOATptGuLv5TNRsSsRqAUoEaBYWsQTKCqp34BkQHQjR1KnLBrynmtkBUktUMiKcQtIa5N1SQPEAT2DYa5S8vquh8kqopBJPu0ORtIij0leorA1V7rvFTHMGvCWhff/CQATwL27xHSByRjnyLqblCJpOiMzTgr9NcdxsQUR0ieP2hreiOucAzg3ehjxJpQLwer3QHKAKwHyNyYWusOUeFgkDoXeQdteUGIyk519iTDz1cTubSTLjWIDFaWSN8iA3sOf++0NUhESk/XzdPINEVzNIcJ+ckedQiyZm9mqX/qNATdYLlhuHAIAIloJmH7mQq5OygXhs0jPpYc9vgyyxqyHK2+A47K/NMcphAcVBMwaiG2lXtYXdWoTL8uMjN4N866Jq2nFFGDkSdBJIwFF0+qLG9LibnltEgWvbE8yxSdiZdO0nSS+nwsw9m5nnt6nbMQpNVOnpTdF4VAY9HR8kRPGdVgei5e5kctUlxMrN9bdD5N7OLb85OA8OlrqZP6/+JDj4+1kaJMG0td9gnnN4QePL0I2J0zkCvKYUR+JC3fhVqcURNh3Q8oO3EB0km8oHgmjC8Z440VzWaEJYvsjcJKVLWwVSd2nO0mkouc7t/L2L17lqK2pxPW64g4V+x++4j43sv2EJZo3Y26ADrvcVlRn78ArxlxSAhv7h3brnvaJKbDykh3+jorC7suZ1AIiMeModtwADglnyMJxGlQXwqoaqTWRwmnN6L2ioIwuRiIJ3bYaKM99pCnDAjLD1CYShdqZMB6AhmcdfHd3vuCJWq58+hssykQpY0sckUMgIbYTpc0sj6voKNoYlG6Bu8HUREYBj0PixajQiAcI3hM7nUbDTidKqgIpXt5LNFUmiP26gRZi/uqzf7WKxmk8ab4gjO4hgojpoCwBF30YqCo6Fip0eNR8mJ1EMPn0ktqJeogTfCss3OZ1J21gCJC5Ky0Txmpg+Cdd3uPWKMpUR+QDdqcuaky4i03gs2HRDteS0SKdNQqk4aEWFCnAee3RuRJ16yWTnBnSHhevLAXXd1ZH23xIHp4IauckRvKdl/eQr2LjGhekdQwSTG6wmnTIAaE0BkGgJJMPrZ6Lf150HYjNQzuTIhjVdVY6X2VjhREwvy01hUmjhxnlvpDbdniY6G6ofb8Vv+2XhFOn5Bi6/2XGLsP1q2KhuXYOmhvQ2TqDyV/SSdoqIGTX+UDsD6q4ASB/bQOlSPAg/ZPy+Tzzmu6IrBGEqWKCGWbRpTlAdTlgePjbaQ2SRbtxLqXxHY6USsi7D5mUUugoIOo3vMqG11YJLlnZANXeu6jc0ZLMhrTDvL5qLmekFk2m6AeWmGtVyiAJQxVyHPbdObi6Du4WrLUKKwWMVvEodpfZN9j8fhEDVs8WNdII6AOERSkSZw/Z++l9mNt7DBuVHBrqvbgu9lAbnoujWr6Qth7RZv9OQzKItyDKi4LEeX8aB6hedzEIqsAiENgyfX+c/33X3Mv/Zj0fbI29Nv+KAzSvNmmjsTOYYr23J6LCQ1WYxkrBsRrtw2xe++SuMaGaWXv1nN52N6bFSubtNSmtUtte7orfShUiP48/SZveSSLgo11+oDCNafYIHk9j+VwjdnGU5TItiM63DsuNlhBI2ozghc5Mv9OjFpoSujlqLy3EnOLwPxnkCgG6hQOljbQLr6uT9fIPg7LvcYI3Msh220yHFlxqLT/NVmdnEQ7uHw33P9b0YnQHPnXQntdTtoYjr2GZ3+fss9YdwE0VEUVMO6pS9g87IZConSgfC3AfaTNuFg1tc7PIm6+npD3jDJGjC9HRE1+mm6VKTfzEjC9jAhFCnOHW4XkVmB6pQ0ITwV0WkHzihAC6ppAuqFHm4AduWC8WTHcKWSzVCmSC7JTWE2Pb+oxAuMgSdwYBMIJAZzRQvUUQfu9GDaFGUJhFG3Y2DqBigEsuwR6dg2XKMoVNQRQlm64nALW64Q6SV3H/AwAkRT83maBNYu2bliBdIwYx5YvsM04LuwRZNAeNQSNpKAb7W66x7bi/YjyaCdSLnMRaMeMcFfsKxFiRDxV7D4wtQQ0RQWgGWhdJCD1fgtL48XDINDsUqXxIOT39SCwWZ2S4/jG6kKFFKFqfxApEGVI7YcUdpZdwnIdpD6pAOOtjP9wp/DQNICqQDUwVXdV1xiOWX4OIFgHWlZ4Tx2EMsWm5n0YGqHCDBTU6YFsZBGQAuO9qJULk0zmh3v/bPMUXle2HmQQiYGwqKq8aS6q4ogVvUuxaFFItXqCnQDwA6oKHgX27z5G1CcHebZcEU4CS1v9DgdCPgSc39qBCmN8sSA9PzoZ5R4TFWibLlXQaZEW5cyNIBEVjtP1ZZC/dL3tjIqvSSGggEicDBWYRQqgIFqT57d2yIfgjqiIutr6kXuKiyjXbIRrA0lUzqxQLrXcdWUQkXY1psa+9MFTR2CIWJ6OyPvQ5a+6RqUEZQjL58skOopgYFoLoPJhG8dSmbzWb8vm3vRS0hl5ByxPVEtzFOUc1maHQdMt6Qgn4nBSRinpHEyA6Y+SdglfrzUFMv93MlKlFPzVv/pX8bf/9t/GO++8g8985jP4M3/mz+BHfuRHnOnFzPixH/sx/MIv/AJevHiBz3/+8/i5n/s5fOM3fuPv6lqUZZGLLlrA8ohw+nQGXWec73bI2i4AzKClU2JYpLB2eBUQ1uRwRh2Efp5OIrIZ5iI5njWLwchVmil2hXwWnQBAOK2y+NByHp7gVu+SDNoiYbrxpHCSsXyYRJJJhUxpGsGs9NqqtQUTPCJyhWpmlQIavcDR811zQVxm1N0APE7qyRCWa4WqToTdINpg6a4g6gYSzwnpGNzIhsXUmdmhL6wXDJ3KwBAFvhy306tcjVieDKij5P3CrRQQO74PtDxSCohzxfTS2marYgIAS9L6FFdIRJTpRffPVUA6L7JOyRmKrc6Gffz83LpQUfT9Ewl7Sum2Jto73DKG56KGkE5ZNp1xEHilp+brphHP2edHX0jszzAI/JN3cZOLsFyYU/VLdeNGqyicp8eDlhkQaASsq6yvlT5iC62ZZViFZRoX1iJzKLxLKKahuwj8ZNdl9eLR5aTkIhqBrOV+DdQhIl+PyHuBvQdACkoTeeQnKixR6x0T0rtaTN6f6zLCZZbcHTPQ1wKybPw8kDsl+dEg4rCzKoVY+QMUKenyXR7J6TNxBHiKOD+LmJ8FUGYpKl8Z6SznDHqftNbGarV7JK2VZA34L4gRRFJbFiOrKgw3I6rnqkPA8ihivRLkJ50rQhHjNioRMp2sVYgwT9erAKrA+ErqFd0B1n2ITWdQi9utsHd8VTDckT6v5pQGaJsOvS0NEoZbYLhhgCDtgq5sTwZqbBGpEdmKCiiUSyWQ1xxfcSP1Uz/1U/i5n/s5fOELX8A3fdM34V/9q3+FP/tn/yyePHmC7//+7wcA/PRP/zR+9md/Fl/4whfwuc99Dj/6oz+K7/iO78Cv//qvY7fbfeRrpSOJAGruGGyFRDjR3oWHuxKykyaerciPO1zZVbPN8+wnChv7TgUoHQbA/fBZJzsPyoZL4jU5JGJephUJxu57fWI4C0RBpcimzMKoCmuVxo0a1fX6e+GygBWQSaVdMf25LaSvonDsaugKmXgNEyt8ZOxJg9305zwOwPVB4QnWzTZ5DqU/ehFRv68HVKRtvIOOdwC8gHEzzLX70y96VriJ4TVSD8KSrukHzRXoOPTv3e4vWQEwdIPQuhGrESrcouru+6YUYP/esM7QzQmDcjTy9qexd8wX3rVjQObNN2mcMtFDVQkX43v/z0YfEPBiTonMBLexuXa5gd6D1i7wTxdt7Vl2/rt+/rV5+SBc9tBh663PVQFOT2/qDsKya2ucfW44ecfm7AXZwta8dJxVI5Dg0XA8RMTOgf2ybSg6p9SJHdD3/uWCC43ewqpdglUerCaBcLfXuFzbapRsvLpxg2sTNsfG9kxxkKQp7OaZghQcM7c5srm8DnETQNAH6G8zfLT3+xU3Uv/iX/wLfNd3fRf+xJ/4EwCAb/iGb8Df+3t/D7/2a78GQKKon/mZn8GP/MiP4Lu+67sAAL/0S7+Et99+G7/8y7+M7/7u7/7I13r2vxbsyorxxQpai8htvArIeZBkH0EHsr15HlvSUQp7Oyy1C6E3uLL2jQnHBeGcNfejrRiI2riH4PI5+XrA8jjBVJM5kJAtakW9uQUNAh0ysxSfjslzRNQx3sjyRCQ1H0FVNYYXtS2uPhGrm57RcpnIozUpwtWOm1XkSQCpnUnHKsW5pywMuVoR1gGefIYaxdA2fE4By9tXKOMjubyOl+fsLucgwUkmlNmT01RYIBCgMRwLQENECqVFqmu3kNXDbWK78E0nnLNEg0AzuAaLZgZMbUbHOWh7bbCWG1gbBSWRIDUYDgAOX8rg9xT2nat0cZ3V6y9lS5+uFfG8etTmuZIxeSdUHiW/wRrF2VjJIEOZUgLfcqCW0Kf2/oeXC65zRR0DbihJ88vemSe0ImhSmLyqg2fTp2oetsqGJM0/CcQBlJMwG+8y6KjNG5lbjq+nytv86w8WeaGwRoGi5yJ5pLUqiUf1/WZ5X+nWZL/uRxTbOUXCpny0lw7HWWsl16y6fQphMZDuVnXsWt0XlabcX65GlEMj0dicCavC52vB9CIjLlJrefpEQD7IvFweCwozvao4vLM0qLA/QouIuKulcodXiSyhNLKEGzRI5Dy9yEjHIB2/TzJnyyFhvY7WGs1zQPFUFEkSB8nay7DO6Q0pREkcm47jAIZTwHAjhnEJhPUxgMioE2O9lnUn66ihWtGhY3hRuWtwqhOCet+wve74ihupb/u2b8PP//zP4zd+4zfw+3//78e/+Tf/Bv/8n/9z/M2/+TcBAL/5m7+Jd955B9/+7d/u33ny5Am+9Vu/Fb/6q7/6oJGa5xnzPPv/X716BQB49Ju3SHFqDzMz0q2E1G6kLqKc0nmrZQra9gGquH2xq5r3qkwmOs2yKIYE7NA8E6AZCG1LkXfC6uoT/CETUCrq6SyLfhjhgqRETnWWa8OZXf4E6lGHk7LU+tCdCDwN0pQxBFTdBGFYcxRKsakRBJU4CSphYyoXYTGIpXp08ZC/Q8yoKWJ5kjA/0olunrBS9O+pXKzV20AQa20HB1FrMFqs/akkuTGFUxOAkENL6BuM4GSOLtIrxQ2te4uBWu1OAYziTmqYRBS0ukwUYkA9CGNMtNmEWRfWivH5IvdGkgsF4MW5VPT7So7xonKipl1o3Xw1CW6iwz2xwueAjavd45gEdbRNTB2VeDMjHhfwmHB68/rBl2Y1U4YUBBuL7johA1XvoQxixEuRnFfIhHimxj4MQRiprCfsDdUl3FU0D6VFrHDGpyo4BIFNh1vJJ1FXSP3aozOIdTeIusKqGpaVHcLiKG0+gimC2FHRWIrKbPWC+YquYWlGXMX5GG5WpHPB/GzA7S5geSKOUtkZYhNw+BIaG9GO2JxZYSI2w2PEHiPkmIhtQ1b0WXNFulsRT+qwuii05PPI9g7NYVKuiOfmLBkaUKe0LTjW92/XdTHaYlC2jF/e20YmjRYrAShAKAE5d9JtF0QeBKAO2gmiSkEw2Zz7CMdX3Ej90A/9EF69eoU/8Af+AGKMKKXgb/yNv4Hv+Z7vAQC88847AIC333578723337bf3d5/ORP/iT+2l/7a/d+Tqu5g/L/eK4Y7iKs7w7QwSn2HQvzibyRnbPxbGJ2+D/HABqHNmEe8ubs3PpiuWpUsoaNkRSYgNtkArawQn9Y7mocXJbHG+xd3kP//yobhmDq8MkgUJ9s9DUqXLBUT/x6dEQdQzDI+cigwa4osyk9QCnNCkVo3Vhc6j1tLhvjDTJxOZwPwUdKbHCYBhfnsAURSCnCvCFibCA78xbVIDCMeq8RYweBWLM7WdhaU0cBPEtt1cYJSkGo4lBDU8OWnn1ZbHp56P343LNnZMhmZHueqt57RGCRvoSG4FAdEupbIVgBuhm1kKHzXfIqIQOUhPUXJLBB1YF1jUtmd8TY5gcgMHpFe97L99hHVTbXO1INZUZE1Tmj8GhFg70vx61nvD70e1Ufd2mqyq2g2F5Jv+bMSTT4PwiZxKWKNMK1OqWqzxNnYRGbjJpt0jUF0C5trtOzSFm1H4khqQmunu/0YbLoxqBMh5zb/OVYPUURrMmnogDyTnqnD81Bs7UEtHkVFH1Tr7TlP60uUNuBFBIppA/rMWPOFWSL9o4O3AxX3wX7yx1fcSP19//+38ff+Tt/B3/37/5dfNM3fRP+9b/+1/iBH/gBfOYzn8H3fu/3/jed84d/+Ifxgz/4g/7/V69e4bOf/SzoeEJIcIbc9MGMp///vikbvL4DgC74iph5U3/QH9K8UOBDAOBpBE/jti7kcgO1I1fQcQYBGBQecvola2HwsgIkLR1Qi0vi9OezRVXHiPpokpoLo7qb5/8AVVyYYApj1GYMg07yuEt6roCwMIab1Tc8b0WRAng3uZEMmV3jKyya6XAquyhP1CR5GlF7L3Cl5YtmdxvqPICHajXuaaLZV1cxur2WH2zzJACV1aAE0BAQVMYGgL/rrB2VOWiie5Yi7bqLDgM3KJOwXieUSZ9xEE8zLrJRu8Nhtx8JwE5g0uMiEZQVe+qG7FJAbuBJyTLwws90syKcc6NTq8Gw71EpCK9WeO7rYsMOzBiOFeNNcPhHxgDe4oQKpAbQWp4oxMaJEHfibZeO3QcC8kSggRDniHgaXOLHiCES6eX2XtXZwSDq4A6lVtZi51bvM74UkonDgPrsrCQUdzKYQfPayBSBHswH111yZ0U22+pK843Rxz4njZJep4j1oPeUiyMrPAQUdW7KFGHK/LsPKoabdn2pc2Ks141V505Vbc5HHUVWzI2KjuOGvh0JddTazCG6gSxT0PnXIjEqFePLFSbdZtF97FmM9vxECJHAirW542lzqOo5hggEQjoWYSsGQhlHnD5N3u18W1rRHMe+vQ6t9uoaQYajkDDq60oMLo6vuJH6S3/pL+GHfuiHHLb7w3/4D+M//af/hJ/8yZ/E937v9+JTn/oUAOCLX/wiPv3pT/v3vvjFL+Kbv/mbHzznNE2Ypun+L+YV4MHpz/Fmxi5X7c2SsF6lDe5pSWFj/AVLknebJ1VdcLMw+uooWnPo4SiH5LaDTMYGAxDOGenUOpdKV9y8Edj0c1xGU8wKpwVkfQajfJN2FBVBTLp3LmfvXBAoZIwmxFnENNNZWHw0F4Ef9soyjAQM2gOH0OrBOlFMRqPWWyT1v5H3P6HWbdldMPwbY8619t7nz3PvrUpSZamBwJuG2FBQCGIaigGNIEbsBGwIwhuJRAg2AoJRLCKCisQEMWBHBO0qaCMgsZFOKDRoR2x8Qr4XebWqkrr1/Dnn7L3XmnOOrzH+zLnWOc+tW98XP9+HLDj3uWefvddea6455xjjN37jN7BaJPtu6bDZfhIae2gsjgxatY9B6p5jEFp8M2wAQXr9TyI0cESYnvxlNrKWP9/WN4dyVMgQogwt7eFEgNHRQ9Q7E9Yb7fvlFfUONXJlpNQAoVAUQGPAhEXpWhEN9Dx5v4+kfCP318Se8XkFPV76sxQjopxmwMoJ6LJ0ZylgP3svFMZNFwGPmL9vIqJef75aj6ba51Wb9QPNSBJe6CsE7ckkMGFmy51WIxMBOsY+50JPjwLWDIfFHAaZrOfUUtWoe52eE12MOh5z0sdzLSpUq1+0Jd4IjKBk66M0UClbId89atGg52D0ppBAkJIAhMqL1hb2cZkeBZOFC9rVQF9XAeuO0jgE7vC3n4NGMtV4eITuhfu59xFrsxF40MCL5tupmvEWQb2ZIAfNWSaicHQ0z6tzhspg9Ad0xMdJkj5f4aRQvOkFTk+TMqobXth7+rXHbTSAq61jUUTGyyOi1vBzHL/tRurp6Qm802lLKaGZx/cDP/AD+PKXv4xf+ZVfCaP09u1bfO1rX8NP/uRPfndf5t7mSxs9gKil8bfz4HlTz9U4pBJMrXF/z1ofQaxyrnD2z5jYdErueN+WFNaIyXJJi0ViQUs3/bPRk3nBuXBoTSd2Qrp8hjDjZ2H4NhG5mpfHDGTpVfwMNCibTpr0JD4D5WaC8BQRl0MjznbjYmNqeSZKW9Xs+H5PWvvGDXRjtotstVh18Hj99YGVx2gGPwwQhW0OIY2U7dpcrd6Na7C//MS6gTjk04bV4bkCNqowXyWiH31GQLmzpHtp4AfeeKhamU8hbBrGuSq1nwmax7CNYgPVuIfLLzzfFyCvdKk4vE3h0e4PtujJae1RzNokoi0Q0FZBE4rxAnQOltsEqklRiWtV/cMpA4epX4sbGaupCnmqAbaCFZ/3C+MgJG3W0zAGz1hzL6x7dWKoOwjuKL50JDcAaTOGktWpEUJAvcIaRXg3bWfOOZrgSupKwlIHJl9tTRNMVVzLP+psCiYLI2UOCNDnZfNoxqKjzoyVTnAxBi7GPYT6/ubHqGnqz0LPR+HEkwAyDS1JBFpKsN9SRQ2UkNgzsrHK2EaCPmfs3mHOrBosBDT6eY7fdiP1Z/7Mn8Hf+Tt/B9///d+P3//7fz/+03/6T/iH//Af4i/9pb+k106En/7pn8bP/dzP4Qd/8AeDgv6Vr3wFP/ZjP/ZdfZcUI+q31ItJDQZx6vc+kmqJkJK2rb5+krHeKqvo9JsGOxTL57QGSRn1mFEPHN4QidZcpbNCLi0z6km/JPmmXhvoWtSTsYQnmQqzrMY68wXom5FToF9YcM5SrFkVjdMlWZuE5xFY9yp7lBget7WYdy+qTRwKz+GhNUFzKMDowsKEy/dMOH/RKthtoXi34OlRN2thlaZ6JrBpR3oqmL59Bpa15w08+f1SLqo1lXexQkpPgG/q1Ban09trTYtqR+ZcPSS0RArB+PAkQjtQLCKXmFruGOXoEbca3/CgJ0AWVaKe3wzefwOW7z3h6fsy6gTc8RHp9VN/ltbTDFNWRqNDMmZQHV4OpfCREDNQtmV81nuo1OHAJph/8xH5Ydka4MEjp2G+jQctNRwg1YbMvXeWFY+XI2G90fYgCu9SGOpkEU90nvX5DV33YvJUbUrGliQkd1BE0A6TFmFzvzdlNhbQpWAjGRbjgxjHZxu1qMNBQhAwNnp2Pu8ya7lE5s2mWg4cc0IjanNssrJjo/XJCuvObU5kVUiYmml/PirDbvlownKn+eBycqKFGx6F9ers8lOCekyIInVzrrhYvVozZ887AA/7gNhe4aUmfmzqEE1aC7YPugMSwjBLQ353BVsNngsN6POEKmbMACYdz3pkhfWGvFpEkXYP1YqBIejt7v93FfP+4i/+In72Z38Wf+Wv/BV885vfxFe+8hX85b/8l/E3/+bfjPf8zM/8DB4fH/ETP/ETeP36NX74h38Yv/zLv/xd1UgB0A2pWk0CdVgnDtp5koy+IbFi7suddmE9Zk10027jVx0rZ14BEO23KRejh5OxYjxE96NW0OrML28qWDVy2uS0WkRaHS9/fqvRPE7QcxsxDkO0MWLuA8asJ5GY4MAQSYQXR/29UFg0meLBeiJcP7akstUskdWMaIW8eYCTU1tpE8UCAC+sY7GsKlHjYzEW82IwWLYAxVUsoc+TvS5JX9GAuZk2WhMtvLXkfqi2x0bb50UzfbXouGzirOVW8wVTE8Ar6YfIKi3GVHO6PADtkqubT3k9wFBjnRZTbxvuYyMCWgrovESk+ZnR8GcdHp2dr0hPl220ygw+Tr0OiPt4OvxKpUVrDWECD9pqGs2LRgEnis0mXTSpxjNDSjZoUA2Qwky6PmV4rmKRw36+BdWfx01OjA02rI/RQI9Rpx8+ldrgoKG/P9bOAMWONUJqnPRetcUEQsDVFRXcgSGhUOdok74ecl5QhIaXCpKMao5OPaqhomafm23/MAhsFBfw+/U6o04sGXqN7e4r8r57OM0K5V2jU4u6+TkV3PY5LKv1ZktxLn0uOp6UbR+ZrBB92H/J5keckgHJAqomBza2i/kOx2+7kbq/v8fP//zP4+d//uff+x4iwle/+lV89atf/f/ty4hAiQdqry0MRiQqGdRpnQJTZaCYEFwcigHaIesk8M2TeScga1/rPXKqNu+a35qns9TucYxtFY4ThGbLdS0Bfegi0etvsyohu0QKVdso1gYRlUvyjqWbpmk85GTGvMdLGx2R9pcSayc96+RTwVwjigzQgjIUK4gajm8SWrb3lw51OI3dqeAe1aRlV9AoWqeitUSDYSaCIBvJwC7zhbxBtK1oopHpWjeLUGV3Tp2BNXdhVM9DRT7BoIhmeZaxnxIAeDuV6VEwv2toEyFd9VzTo7IWhRmEBoFSsKmqejUsR1Hvj2CHv2QQnTXqL1+rSnUFOcKMxl79m1VtxPMy4n2T9nJEvlnHPBgGxx2BKYVAaT1wUOq9j5kktr/r5uUQF4ChrgpoVlsVRdRjRAPdrKgA3gwy6s6SwXw2XuIO42GCpKaEgNosyW4b7ehoEqka/HFWtMLq0jzy2rNJQ/7I88lMAKfu1DlUZnOKrw3TE0PYm2sqazFdoM88KWTXMoIlt9FwbMBYGF0PhOsXZ3ARXF8l1IM7vUCbYYW42OSlvJC56wnCckW2vjKjOvlpHFsbnzanEB5u2eXWLO/KvEE5NAXRtk6BvQZAkY7BecwXwfxtRrqoAkytBGHtEYVg8KH7AwQgAcKGvKwaaVLt3/d5jg9auw+cgJy1yZxh/SQCMY8wXZWbXw8MmXTQyqGHtwCs3YboZnY3qVGyhC5I2za4R+CeIC8l2gHwsiJ9+1H/zmR6dV1EVRKh3h1QTxm8NMyXBfR0jgVDOUFmrfFY77JudrlLm6RLRShSmgeZnpaeKJ+ySvHYdz3r2+QJURervJiy8mkONW2+VqSHq25Uc28MyK4I3gS35xWnr+egPutAaMuQOnP39gyWmD69aFFl5I9EN5XLFeLw16IOC92c1JAzB9Qa9+vRQWkBffFSuhyTJ+gPE85fOmpnZIrAKxwSQD1g8b54lmBH08WnjeJgTgCQLoLTN1ccvvmo92YMqzgyQ0SjC4FuLPODoNo8u3zpZFGvGw37fmMG3ny9gd9YGxZrwrdhNooApkWn8GXq4zE+4mC9FaAalEz8rAGga9eVW2UsXj5OKCfVHrz/7w3psaLcZ5y/pIrkmvS259Y0egxGJZvm31BSEDRvCNi9+mKNI0tVEtJpsmjf21joucrdBM8p0qpEDJksqiIgsTthjHYzKcmnCvK7K+jpGoQSvgwq37DzedsO18ck6rVwPq9IiS7T2wXpUrVe6jZhvVWDdnyjtVwtE9ZbbWMxkoacyYZw3nQerbeE68cWXVreRiFxbQBITaOqOvn+MjhM4gZKlfl5VWJLuc1oE4GvzTTzTP8wkWlLsrIyCUhHRjpOmv6wv4+MZjInIpoxrltVe+8AoKUoDYfXKz76fyssefmEcfmehDaZ4+zQ5WB8WpZgmFIFkikBsZMv6g5qec/xQRspckjFtad8wQJD3sJIAu49W9uJnnPoVdMtk55uZX2PQ4nOihk3TFMVoLVojkUEdDxoJfmOyaVy/K4oMERQTJFEb+b1axTI0dF17FgKQDdqZytZvVd42VZIvDncOBXYZ0wRwUVXLY9BlzUoxGTRmUZ+axgYehg2UMD6Nt1Eo8exLoWvK+jhbANg769VDZQ0a73hSVrpi0Goe+aR1LU2AA0mP7QzZJZ7KEfGcvvCxHePnO2HaJPslegkiqDSUlUHgd+aIXHZnZzQbmbd8JoFX+JYu000oih8DFkh6nTuKOEyXUCHX+IZ+hhbxLGh5CdE9BYNFRtAIVKIjXGPc3FPztdZ4aZyS2EoNJfnOSdCWgCy+hiNFAGC9gTiSj0CFXRo2Z9VBQCJsg1ai+5brUeWQWEmoBkNnJemMJDIoOKy3VhlSijHpDnQswq/eqE27ZNsbow8irJx0UaAw1EFYLHWExatHJXhqjkxLR9pkxXDkzmfc4+kYnNu/f9VcxIbp1gjHNG6vGqITtpFIdJ/J3MyaSlK4rKifBetJafQuwTbRAFfq9NMQDMy0Fg35tc6QLwbgz7nTRQFaK5qflt1Dk2E9c77gmGAWNFRCeqvqx4h7ML6Ovs8xwdtpOQLH6EdTgFhBHPMDl9AvAqSbVDUgGYWfCQAbFp6ONQCbBZ7T0KzGkhb/Mp5bh2CAAY2TlIYYgyH/WiiPZ/WonIz5k3ydde0DIjKeYiAL0Ohnuv/ERR+2tkoh2NiYToGT7R7oy1io+CjYtMvSI4z2nGGU3y9Vqpa7VXkWQjPz+0HMyjDHkR3LtrtCeWjQ0R1fN2L9FGMf8AgwRC0t1wr5ncVXPZAvJ8DUe/kKuDOpmpZoRlnHeUnK2B0gdF4CO4iAq5r2PMk5gDtlNqbJdpH2IoszymngzoohylkagKKGWvMjOgA1vnWjl7j0jbzdMOUGynq5sjUQ0K5YTM0pj3YtMU4tROWj7IK+Z6UHJPPhkb4EJB6zO40dWjKNsRD0ut0otDI2hwjQIes7bqI1G+RRCi3uiW5BBOaIQfLCiICPyVMQHfW/Bgfu9OuN21uxjGESoXF+/UnnD3qDFZ1BO004egMe4Hdj69vrn2tu+7dmPPRtIDOO7Li6o1kUPX8LoyNynDRZE0JWF1idiIK7LkmK5NQ4oYf5ZTD2DqqEcYqEYAMmnS/Y3PwQ2Q3iD8DPNoEEJVu4jIMgkWBfEXUEl4/IpQ7HT/JFmA1LTSPot7PcXzQRurp99wjz8eYRHzVlhS+gQVk1nTzcuZLG8JrANjjowFfeCLSvRXbiF1yRbFesR5A6o3ibE31/GTzBF5O4JoREkuArspSdFM6X5C/lZDeTRqxXBegNjUMr04Wzg8swnNCtJgvCZKqelSNQDtLGN52IotG7Fp9ovpETAwxlQS6WnFlqeFZtZsZyxdPutE7C0l6JOo3LWMw5xHS6NW70kBKClUmRvnkhPP3zBAGpoeE6V1/dpuKfX9WAz2boPAtXxYcv0mdVTYeXid1yKinpHmp78koNwa/zL5pCKZHYDo3rTOyPE3flEWJOr7pecSHHolvOiCTBg8O54y5tnJKyHdHozhrx2f/DlRRjs8Cc5gANM3BtSOj3qhjkM4V6WmJqIqcvi09+nflBUmMcsO43vfEdTaFkOurhOUuYblTckw9aPuFw+sV6e2CdszxnSoV1K/Vi1FlYlSrh0ru+cemyBjZnq6sAQBS3FCpgVrudX4ef2vdtupYtSVMKhX8tNu2xgjJo8/W4MXOXuSuXa8VxmpHha55Geuu0B0F9giIuuNlRrV5GxSf1m6cALiKh9ejeWfaMb+mUKmtHxOI1ZyNhJaiK9UDFBJKbdLu2N4DLJ01VCk3qs7eshZpz2+VAViPjOWVRoT50nNNXkQOmPCBaP552tc1jtE8oLWS5piorFqPkFTRRFt8zG+q0u05o9zqPWubD8RekaLY8TsfH7SRqkdVF/AjGuT5IQBBlOVjbTYMANQ/p2Hz20c5gEYgvN10Npsu07aBWWvP+uuE2OiQDN0cTWWUaPXWzhW4LpBSQTmFzAmAZ/UP0Zp9uN79QQA27xqYTiQvfGRIwG9YjomDeBB1Hm7fqoSB2nzPOE6718TzMEZ/D5giU+icKStsN/7jvY5RTm1KU94XRw6GJIRUJ2vFEd5Nj7LZii61SNc2Uh4i1924bOV1ttfoNVnx/CwX47Tilrn/vzENYbfVi3wlTi3NnCV7P+/v1aP7hi2xwvJ2oVYtMIkb/bM7bS4IGrUzi+WUMo9kyj6OY7DnDpxf0zOm7fZaoyliwiaa8OgWhIAKUdUhA7M6Zk64yenZeWPM9oy//Vui4JixWcSe/wrHqt9bPM/x/+OENjZDPmmE7Xx9kOjXsX+m9XOSbMd0LI0IaarkkZzYtUrPd9lu7tJu9eiRvbIVsb8nG3/RGw5n4cXxdIMlAheTVVV56sSpqoXk01NBrSkIEmLfiWTh00vj9xnHB22kXFjUE7jpUlXFe62Qo0BIE3/E0iV1MilbjnyRdugCMmxO48MKhQn7z/sm/2HeaJIBUFyXCGlV7BdEwDTHeYkIOB5Qv3CHdpyUsvp41ZoqIq0kv66gJYOvBoVcrL8VM2SeIMfesvqlnBRfCuD1Wb7prRV8WUErK7xmHqs4UxLQyAwwQop5S55I9w7ES+/662wwyYx6f1Bpoj1cZrVmUUtDOuHTVTdvXv09sIUwQjqk50iskdhYA0ZWErB3NnxcfCyMjMBlikWUH00QtwDTQ9PW3UsNaaxNYS11TT+q0ntvnQuOn3JsIs4i1B5JdvkrTIrIGKO+Z3jk5BFGk65f50NnEXEocZC/31RIiCKHMEbT20R53wSbN6QbDq7A8bf0ORxfm4GeM9b7GefvnYKJ1rI+GyG9XypaO5bOJufEUIUMf96tN0rULx83PV0DQkC6Jkxn6qUWNwfIPIGuC+jiF8ndOI0yUyIdYrSSD6W4WzfenKJFD4zpG3C5uAqGKtUEo9GjmqyRb5vJnqc5tr6WRsP00kFq+L2EgVdjDA6Re8/xSawHKk27jd9wOBK8AoD2/+JVn31+qDga9D09NuSHFRCgzayMQtvrupRVv3Y3wFy2KjFbgYG+Z6RzAS+MaTZh5ElQj4T1zq9fe9qFkPVK8HrNBnxXRbx+fNBGypPVIe//uILfPekElVPvz+PJWFLqpgxS+PvoJmi8vrb3+Ruf3OatjR62zBPa/fGZd+e5FmUSEehgRsoWWbu7wfV7b7Dcs9I8X2fwqnIx/O0HoBQQM9iNB7NCZUTa7dY6jmpifJfsvJrsjNXNRNW5WO8pJpOaMYIEsmqmEQGlmhfkC6R1BlKQBTQvIkm7/opV5C8fH0Cv5s1Yutp64PzDM5geq21UndkYxBU/PK9ApPc/jKEr1T+LIHJSuj9RbOiUGWk5KCxTgcMbwem31t7o0ttIXMvWQJmBbKeMcspILi1VBfntRTsNM0zlWhmS6/2kEBZsA3lybToES46qssdiPtnGvpmbbPdDw79NNw6UCpqyfif0nmj3OSXI6CYhDGt6uJ2n04Pg5ptFKekGx9XThOXjjMcvc9Cmueg6aBNQ56SN/55qv//DFMQDXipQWpQ2aG7UDJSxAZ3slIPFp3OifHQwFh9tJKJCC3EoW1DCgpGZljXGRA5zOET+TAItaBZ12ve32XI7dh35YrTzTGjGwiu3vcaJFwrnltA335eMVUtAtWWfz8DhTQunaHq0AupDsoJgzcelS0G9ySi3yrhMqyCfJRqQ8qKlGFwapgd1QulaNfoFIN70kNCVKmqf467qIYmCnBH6hl6vF/lEAi2qBi/MmFlh/3JQAsX1E13TSsoxea1Jc1SULEx0p9fIJb8jiBPOIultGiyRPEBso8HwpoEQb2Jmk3STdIUHV5s81Hfk9PsDZX62+OHY/ZhIHqMAD+NdXiWT5ods81XKtiWjmYGpQxUaqaGzeQgb/N/JELAW9H6dqiQxXNP4ft/ULM/VPVX9LNfWFTKqEzf0HimbwU7PN0ERQCp1dGV8NtUW+ktRqsjm/XFt4znsGp99rsn277Y5joKfXJysoqSR6DH0nohZiz+BMVdBzVVF7JzD4ncyh24sFSEVE1CzvEDE2D3D/Wujs7Q3Zq0/w/jXT9X6dQc8K/1f71rr4xwwoUFK7JfYenTgXYBJBNI66UC/6wWSDtCjxNFTrxJ1UgDQEoPYo+4+dzuMjM1zd6bqs3EZNlrPCcV4bOBainHp3ZrJOsyiF9sy+jP8rEP6T2zKYvdpzGLtZWbOXyIQd1UVb5czHj5vx/m5/X/LI3vNqDPthuiJvAi9KcuVRkZtXPtu/PywnKxfozM9vUW895HS5zEY7qqIlkKa1MfmcxwftpEqAkrSDQmRJbp1ZNQLbyFmCQBBuxQEpKMFhSnYcwCCvfasKh6AVN4UE44Ua7oqC2nj5fn1+iLziMg3qbVifr0iXbSanL21tfUsosTANGlTN8fRLYKgWpGebAwy9/u08aDa1Pu8v918p9etuPhn0Nr9XsZNBFAv6tE8qvX5Js6FMYmgXbQdhiuIq9c3LCwnGmS2jsUUyXcAVoCo8GJ6KmAT7PVnSGwJ8NrvJZiLU+6GaXiOnqMJiMNqPtxjnt8WUwWwOTQpRETjs/LhmJUAw0tDS4T1k2NftNZgTrIWy7qzML3rzM2InrzFum0OUah5M2uRbW1IT6U3gwQAtgT3WyW2RPH41EkD0hSOlFuLmC1JHu0lVqeId/gpcourWAF0188TUpr6/EYC7ouclZEESDRJv35y6oxai0JpMfg9JcA0otWxqXCduI0iikPHB1MSByB8BJ8mK20oEQXocxpy0gLVCvTOAubk0arPn23+d3LEFiLna8H8Dpt8VEuEctTapGaJ/3RF32TJnEt0YkAyR9cbcoIAeqevUVM5qflN0c7TS9O6P+99Zf3t2sQAdH3mJ9NZLIgu5ABQjxkkph04d0coXS3qTIT5bQljsY9iI1+afU9gbavhLEh3indCv8KWRzaNy+lBQP+DwoCPRfK86hhpI0SK1yEaZX2e44M2UmlV2iQAeJFhUMIB9fYBNCYUo+1Gi4bawI9XhcGY0e6OaEcviuUwUF5/sKEHLy3YdRG1AUp6OC+awxlUAsIwGFwkvpn6a2tB/q0HhTvG93vDvGyab7dHLXgdPqtwz6qbdGKwe8/Dd8uUlB7stFzRrrjrq9kEYT0XB6RrRXpcALReQ1wbcC1dZ21V2rw7BWBruHZR5eV2e8DyUUY5quSUMuU6NEqiz6SZPhp7d1YB6jGprllVqBKl6YaUsrbTKAQsvcA5cjUghMqCi6ZGzqDFeKEJiAn5W2fc2mYf77WxapM18ksEceLKOK5V+x+VY8LykUI0yWBnNHT2I4D5bcX0domNRyaFm9mbAA6HpAnlNmO9Y01AE0Xkpb2j1Ljlx2uvZ8kW0nlEQYRmHQC2JA6y+S+9FGNkeIrCsS0RcNh6V2lpOH5bN5n1RotUnXLsnvJ6w2h5Bq+C+fWiec4qmk+yYl6kgzlOErWGlJPltvRaHFKux6SF2axq9NSysse+zchFoes265wYIzW+MrIXENtc1bw0d6No60HIcsL+3dei1+3RX1KDf/0kK1RHuoEnI2i5ELEqT9j4LgIpADcJcgo1wfTY1FkpmiOOQnfuGz/PKRTWVcYrgQQKB741o2uOQ8tauCusJQPrjbJc02JMviqY3lZMb9QStEO24miPmprmtGERD4wZaxZN9wkz5N5i3hQsvGWJk1wObxtuvqUO2NP3Mi5f0HvIT0A+9/zyPnKqy+6F9xwftJEaD4cmkBgiO9aPbYxeZ+OeLxwaBAICAtEmD7P9Dvuf8U8e2WzeaF4IqSRJvG8HLWwIGj4pRihQpEcCw/VEPs1fGPMwLjuDQZPLKMhEAoFGmqEG7hOU1KC/l3EzQij7pKolyDfvHYeodXhthFrGCBj7123cPDro75e+ubzv2BG2nh1NQKg9strVx4DU6JFfR8Bm2GyIoO49epHws4JSYAPj6XteGCePChmIzsO7WwzWrufe9pDn5ksRzLA4z+5t3dnZfu6ZOHDz2hfRQlKfJOMzY+rF0VFk/8ImtJ9Hm8hdPXgiGjY26eOyn5zU51A8l72juvtOst+lyUY5Kt5n10FWg+R7R0B1/lay691fk3Tj7WxRbwiaLhoZbxRThHt1tx/BMMRQYyl9XrISTpSkZJGcMTOp6TOPHcEdtgOeOfJRnuLftZkHg8PM/bVw3m2dAHavBcbew7N5Fu8JdMb++d+l3ff/z2OsogY0/BW6Ca/TMd10KZqwh74WLBaH6mrT6MAgMzjhAn2Dbakzt2RiZS/lFLUYcfiDHVpSjE3bou4D42K26GqAPQCAJFm9i0Y/tOg1ugYbHDby8x0SNq0O7HpC0aAJSGpM+nRtQVdlh/BekCSK8zh7zFiFPp7P6MaiEJ/SURvyuxXpvAYkKQSlgRtUoScFvMgzXWCszZ68DYOW7BwtxaYSdOa9q2aRT8jhLEXbylte0qMRTFPUzfhpNpp0EJA0S64D9TQrk5HUUwQp/JeftAi705sBb14JWGLcGnLy+vxZjX22PJJMT2vcgxrRtt0DeBtJqLaiIJ2b9sS6TcpGo25IAUQ+KqR9WNeTtlwZOjYLkFevNSIITyhHCeNXZ9sY3bgnAmRCPSXVBXyXQZfSo1Gf38XGyvLIm27VRJiAgEf7ZihIj6vWYDHbGCqsHt1umxIkOJyn1g2h5WpoEbAXO+9zMU40mVIo708PzVT+h1Yd4zzZzH01TP4vFV9rxhYdJMoAhFyTJP0+h+0CHofem+QUkDKtApf7alnJC+XG6qeaAGc3CpZ7snOUkyEENCPNQ02dR+quxL/PR1l+NYhoaTBcMAbrScsiyq3VdwlQjwBgUkir1uSR9zJbBWX9fDS/D9pINaa+yQGQYwpWUb5UndDSQOcuXRSRj1hNkhsEq1NCy6Bj7kllJwiQQDWLFN9vxwk0bSG0Z0WErjbuPYRE9HsMxvOKeyRWAzOp0aPVaLEJQG7KimkCXKz9Qj7qhIHNlZGmndkSyAbrkYfpDJShSt/EcbGSMoIuWkCMnEIShTB0DfYCQGaTQeJuONdd2NIUG8+JkM4N+c0Z9HTpzftSsuswlpZ5wgSxSnaNcsjo4nu6bMsMFlU34PpCI8lRAicltOOkzEER0HnZ0NpFBHJ7QLV237zaYg0vukeK1JQZ2CaFWngVpEtVSv6lqP7h2GCPCO3+iHI3m4yOd2Ql1Ye0dh1u/KMQ2SJ9vhbVpptNvZy3m8MG1h1eo7Wqen1mLPdJFbwJkdxWB07/dbUN7bklSFdtbcEOARobjM8q4VSPCelOVSuiNkf6d0OAOjGoMdIiOBAhu4NUG1zdgYqxEpk6rD1sjrwWTINxc5jbSzIkJ6BkNXTgcFjbrGupQddlSP3YuHqOjxxpdVkld8I85zJxiAHnhxXTGx3P9S6jHQyOXof15PPJHWfCxtBzGCmNgr0Fjjqoamy9Yy9Eaw+d9OCdBXgV5HPR/cFgN81HAeVGyR18JUDahmAhpC05VKqLYv6G9Jd13Ka16j7pe6QRx9w4eXnJqFgjpEaqnjRnV06Iho/UFGlgzx+vAraofHq0kpzPcXzQRupz0xhbC4MULKG9BwRsXovq9f1mtYtltx73C+cbYYgGffjUgpjxInPr2Y1amOHnHBmIfuwq+iPKIF0kTrXtUZoYwQCd0PFS9BRCc/TZ17i/ZIdHxnHOCXKYIjfjMkTOztzQzZ1k0fQhB0y78fAGA7ofL4++HJ3yzZy1p9Hmve87hjELaq744kdAO+xUeX//vhj6vTCbbH5XdXtLuLtD4ffcZAsdv+95iIBAPWqw5+AM0PhXh78Xzsa1+XnQ7+kFxtwG5fIozVlvFhbvz+33+pkMTr8/e694vo0BaQPMG2tyO87iTkwTpeKv9MwAPv/el/CpTkbxqL5B83MgNgp6U6brcLTGuhXQoJoSckL9+fWGm+P+4FGN7W0O8RG0E/JL1w/EZxgavXFF1HvGGsgddZLqz0r65/en9vY5maOVfLT0SQYz2nwKdY6e0tLT+tAPCHGo579P3OCF4wM3UhJCrM+QnuiYapPFFR2AqMwG86Agzs8XvS1Qh+24qNApG5QXrDiPwvafd6jtMEOOpqG15s6MGza0McJReuhgPOz6KPJntcNnoiQIfZN6mgA6nJITEqCeuEiHOiGdUMAqe7RfzDIlhT/tXvrYGmkEFkXu4D5v2SFJk/PtMIFFsH7xFg+/94hyJMyPgsPrYirMNsbDfuHsRloLpDH4UmJBez8d11sjcGeJ+T3YMfbOoTmBjhNQ8y46o3gmUZsl0qnoLpRaG5gI+Th147S0mIe68SftZ2XndaX5aGFyRe8J5PmCtQCtIaWEQ22Y5qxyPeclxFk5aVRIrRnjML0sAQVo3qM1SBOky4R8VI+7HmC5C6318RxSugC8YKBFGzXaauB6NM2RewqI0J5Jy8Zua4ieSr5R+5wbDb0kfmYaxIq0VTA1RbO9esyqL1gF82sCGxlCm2IWjUIOSQv1E2F9NQGYkM8V09jWxgxO5GUAhXt3+auYG4xAMei6Ii0EXuvzyBDd0dAauZ1ivju7RAhxacoBf+u9CNLagEcdOy2ulb70fK0P3RVcnmh6RDTnPLxtOHy69vrFozZ0LCfuRcgGkWs/OCVykGuPNoEcMtrtIaLJ1ZTXHV50XUHXwayDqC0XAGcKx6VN0K4CQKhSKFNRnrVWed/xQRspNG0LQOuu6BPY/t6adsRtlpRl7yn18iLf1JX4xPa6GulerhsouhpTJ6eu1gB0j35KASfRnHvTsmsFeYbfNzqHDd1YeAQG9IlfrfDOPgfPbVj3X/K6qNIbL1LZPWqDsNC0bUe1yax1GwpJKAxhE9uFQ/061++Q9bRcgmSDMmXC8vGMh9/NKLfAzf9UdYN0Lls6O/UNXanm1eqPapcBGjcGJwfwqGDRI0lvY+D6dTJrLyYZHGD/vnHsIyfg8IzDtEzaEsI2KYdm9Z4tP+LflYzN5+wx827ZHRzfGK5LdCtO1wXJoFRX8CY7N6UGGe5zL50U067WgEp5aUiLCvtWa1/eJqDcaruIdCE1Uquz0TRK5KgZs+Z6Y2Q6KLvrF6qn3tyLbqK1MJBupIb5HXnGQRcRgKpDWMuWKKdgwnqXsd6bwselgt9xP18DcPDnp9dRjr2JZ37LsU564f2AXgRstTWZLkMUqh4mekznZbuGNh8S0DwBpQsG9w7FPdcb7eqBgG9JoB2/nVzkRrBfbaAvzrCjCjAJ+CyYnnTsp3fW/boK5JhDmLYerON0Uwq971JUjcghEtcoSferelKG5XLPkYsL3cIYJ4uqnGRaoexHBuoBqJP29AMcdRCrEzNH7XMcH7aRgkVK3idlgOc6vTaUeegAAOXTSURBVBSI/I/bJDG8F4M2n4vEeog/Fny6h117zsDzAVQ6HPFZR4iUWpRB/pnWCQn6BurdU53kwehQIdCJGPb+TazeWhQj6n1RSMJsDu/sSUOvmURabLsbX7twkEEuPp79TbvF2poZO30m9ZjRJsuNRJIeoZBB/BzbfxaVugHZf6fDny8JEdrCCSZj5AsYMJFOfX0bLW6chJdgqNLAbI6GRwcDW0o8GU42P11qppn3OHjdtIftxvyJ7O63Wq8lJ7AAz1mMPk42dz0yggDZ6lKoAWkmoGmJQD4L8lkT2i5zFcbDnACyjUfzjehsPL/0AU5skxJMWiJMN1lrgUoDULqhcuOQeasgAVsjFtGIwV6qDzcaGvSox9eqQXyqzj3MF48+Yt0PYwp08tVgsLxfVoyDpwmGkpP9uMdhUYWqXCQEKcjmVXQ02B/jUmaCKqBjm0ONfOvwsYJgzj7LkW2IJ8P5DaZrUwLdzGb0M6Q1tJtZ6xytN1U5dEM0nq9/B2Jv9e+QZtuVWJNDy695bqtNjPaMXvny8UEbqZYZdWJUIzR02R3dHJs/5OMEWo8dmhqiAAJ0E7+ZlDDgieed9xfdcD3JPSnk8p3w7vBUgCAeuFCss3qADhe0OaHcKXssP1XktxdrbEZbhlTcQMfOMXjnzgqUKaN8dEK5nzq0aHBCelKYqR0nlFNGy4QMqJSJJ+WdDgvuuDlDYQofHycuOIHkWjB9+4L8kLC+mvHwu2eUG2MAWS+vOmtDuHRiYwJ6lDZEril1Ze+1bBarjucwFo06Dd6YSIiI2RbGrDALYB7guLDcIWktopCRGeaCuMES9PfYmMpx0uLopOxPbwQZ8jUGX/KixePtmCGnCVQbshcaO1N0vwmWqvPUow/Jfd6N+a8XDFt+c40Gnm3qUWWdtXCUTdKIVnVaeNHIVqaEerD79cJyANNTwe03+qa0nYtaL/X6/0i4fKkhnQnlNOH47YTp3HD8xhXp8QpQMnkcQbs7YP34qHPvscR8V2Zu0v6NotqAENEavrHujXTs0iODVmVWTua0qoPHaIdJo8srIM6UdUZstjXosk1mPGitmIxQIEyot7OOw6V0ktULDowWuuYgWSyvtDB8emyY3ypDr2W2Yl0j6jjs6zlNos/onJwiSnTjl5be/cGl15C1KLvNCZLJoGaKnJAQqQH60gzhw3a6HQjLK+sg7AoS9l0uibU5BkIOm9Pj7wlHMHs3AMF6p+Sjsn4+8/NBG6mgnhommkg3OU1d9uhKvC1Ea5qzKc+hKp1cKQomo3PnuGFUAVAVdz5qsSRLUiht3NRk8MAs58CiLC45QJOSBBNKtQdpUJpMCet90pYPREhPrN+bd3DWUGTqgpwg0mQxUTfErK0dlnszhnY7XnjKazUhSmNHrdSNERAGlexWeqV+92qpUSSGyVrDo2q3U7qdcPkC4/oJdKI7QpWB9YZQJ0Y2cVlqALNt6kDUd8WmImUL0eyKn11YVZBNhQGx+YCBRgRh89oNXw967NL6RhE1LB2aikjbnuceXpaDbkxOI66O/xdEDoquqzaXzAntRjsxp6UhPWbQxcKU0pX0I8pi7vNRUuTRgkHq1+lG3PMsTcCPF/B1iKLHjTUUL57D5e3uCLmd0LKphtj7+Voxn8t7CUv0xSPKHePwex5weZxxuR4hiVHfEeY3CemJlABikuDtOOH6cUadCUcA+S3UKLsVbIRUloiYaS3baAoWIV24t8uARWM+fw4JWIcoo+40HjPHe7VJoK4LPhv9/2bWiKMJ0lqBSp0oNcDUOvYU86CeGNePKPI4VDUfPba6SVdSMoZHi8VUPw6McmSkxcSpq85jdS66c6bztyE9LmbINHcZsLMZtBCFBoKQ0TJhuaNNiQJIYbr1XqxVyPBsVyBdaBuNjodFn/msEWta9J5aJlw+1r2vQgWXhYE63MdnHR+0kVJlX1HSE2GT7O1yRvavh9cetgP9X5NSikSoRyRARCpxuPft+lj7c/lnRumjsW6GOxQUdVHmySs0ljqDhv1vXc/Mv0thDvQCT7HEdMPmvsIg+ldlvSYSQj2plxXV44k6BDdCXw7rBBwCLYTcb1Seg9vUvCDgvSj6c2/O4QOY5yX2r8ObcV87g5A6RLRhuUVuaHdd8T1DBGBjIgnKdsrUx3jceEwWxqvuqTTgujyLnDWX05SZ33rHUtc3DFjSj9ZhNclWdxZz8IVdwKVpvJzB594L3nx0ESaKdigbIo5/jimgsueQrRlX4g4fSo82nx1eS0MAL4TL0wy56jxXIVpgvcuAnLZ5z6w9qmCkDT2XQeD7InmWyMtsW1qQGhqT04L0Fi/aGsPG3teicJCnxA0U95+ApZyI41Divh5wuHefI+IIQ8ClUAcpa+TitOzPbKdE6EXK3CEyQPc4L26Pt6+Do9JI89zCRo4QIBtS4EXxTKgbiE76PdupqZJB0+idmJ15uhl7i5TYXh8iKC9iDhhTMDAy+3u/0/FBG6n8uGK6pJg8vFZrtga0mwnlbtYBOuTOLpryM3gKgEY73j59UFyW09yZcZ6fag10da9Meotqoq5sfphMisiIHavqrJXbSVWCGRrS+15rD61Z4lcYyGfV5sOyGitRz+1CqBDZ5obGTrpMAGeNIm3zkKT4snZmTUg3SqVtFlXohEvmSSrMkB6ugKjH205dZTvICxsDzj2/5jAZK8TXJk3S5yeb6AVRGS8XczCMmDJWwj8TDGWG3B63xbdAr6tq2MCi3lI72n6PRdq+UEwRXJiUbOUtS06z1Yyp1FDLjHSpyL9Vt8QRi1zT46LPqE5oadI9cekbMg0FpHwpsfjanNG+mMHXglRqb6sSEz1BjodoYOj5L74Y4aJJ5B4VRs6KHDD0+i15P+baYi5b5BZRhj03WrUAvtUd43HUebRDMqPeZW1zkQmHbwPt/zrYOFsNz0yoh4y0JFMBr0gXhbrmNwVeSzTC4DJ5jgrDJqzPbtPtdsqoN1No2Xnej6p05lo4MBzwHgx2rccMb7UuZvxJciAKJFDihCMYQBh4dwTqbd8nuGhETDUrCy6rlFSb1FlIK0wmTNGfzUFem4SQSKpWr0VFMD2sz/O3VeJadL/QeZYAc8S8zkm0lumoLTzUAKkq+yieq/VT6mzwqvJGY5Q6HtrQkLSo287n6hJpaUjnBjTuElHG7uMKtM9ShRmn/+d72/8zj7RUMK3KkhMzFk6NPqReRZ81T+AkBXJvZyi+dXn/OETCO2pTCg+XgE462CdSI5JQRls9qSpEelpBtUaxYT1xTBZXDI4yKIs69P/dIJXOBvJrGwqR43BP3HIXMJZUvy4YDZnQmkTjwo3ESTVNsERaqGoKDWT5OpgHKMM1xybpYzDg61GT45wPby/tXqJFaK58EVHPQHLpG5RAJqV119N26lKznky7xaSFjPrjlHgAVkui529EPTJ0GI0ILRmmPzHWm6yFlgBy4q1XbYYNSwEVjZTZBDypSY/Mh+elZQQmKnuaUA+MTAR+MaFOCicOuRMAwDI6BNSfuxNlTCw2jBS37s3SAE3HOXZzaa2qzOBRGPAis5Ng3vmBLbckOBpVvZ6AelDoSOcbIT8C1Bg5E/JTw/Rw7Q6GR8YDvC1jFJd009scRlCoRw7IWJ0QHfdxUw8h5lmp7m1EESx60iJn7oX8156D3KumOxFCtfFY9UQt1+tNWCVpyrRlq1t7kqiF2jYKRSf7sAnauszRxNph/GyKG/vDncPW823ICbwwJAt4ZfBq7E4r4KYm4Cddjy0T6KDzaCMMe1ZaO6+I9bPPRxYY+WqMjkR6MTMh5KHiZ1DU+E7HB22kxGGBA1S9d0pRUS5JMWoRdJiFHFrqm+izDp7jRhKLo8X/Y4TURljI4ZOXmH5el0EmpnohMBNIGtpKmyJLSUZK574xBIzTgEgG8GBZADMU3CPogamkk6tv0C6I6R0/uwKBR6QDRdQ3MvegyXv7DDCWQ2nuoVZRyKHp5MwPwJxVcifqKjKiv05agOmYFJuP54C+YfnhdHhrb7A5BuJDDAEMr7/KwC6yzdYhVsCMGEAZaFMCz5MJojrcYwlu4U6b3RkTpZwPCfgBHoWxnMa5oRtn05V/zAFjvcRqBCt1vk1p88jFYdkB7lQFBmzmoEoa9SgS1TcJqwMb+pPpBwiYcoe7HCFvADKDdtuGb+wK7TTkq0UCkxd96lyrNxKJeBIGX4HjtwnTO9Zcz0uHDAbK0Ip9fViUSdgz7a0ufK23Z3MpCm3XpnWERaPtBo5C2s4YHPaMfV3g+IwBVZu3cpNy0tq0OqtDxEX3KW9jI76Rj/lQACNk5s+tJYD9+blT6Pckw7zf72XSyymoCBIcshuYjrH/dA/Ua92SyZslEx/m5PvUUB+VqUdmq0ZfYhBnlH/w8D3D/3+e44M2Us08mGimNhoYawzmeQ5PwIeUiUdBMkxkf9D+b2X1JoHYqEevHva6fiH1BeR5LH9L4tBvS+cCvtTwlpBUssSjlzoTcMNB+9XeSeYJN5WA0XOmrVfHfi07qrm1jXAIUc+jydv1FtoaoAD5AlBROaXpUa+Rz2uXjloK0tkWhbORZPCGmLUuY0q6kVvRajoX3P/fBeVTxnpS1lA9ap3O8nGDsKqA5EtCWjgo0Ax0qu5ujPmydMakHSNEOOZ+Eq2Y4Rtp3whcukaYsN4nrAd9bvUmgz+6QbDhDP4J9pRLXbl2oX//PKHdTPGsx9yGOg8tYDpqDfR0BZ2voHkC3c49B8I9H6jTS4Apo9zPKMfU6dhNwKs2qCSHea2OKwwbmSNRAbKooU6sTMq1gp8u8GJzNa4c9T3PRWZFjRU4Wm70uUc2tyt4UQLE9MQ6lxuDboD1Hli/d8Xx1RXrmnA9Z2Bl3PxfGadvZRVfxbjZMry4O+Btf27TbuwTGTOx2bgYEWEdpIh4yGM21b8D1EFyQ1NPU0gh8bVqF2c35r72vUieqRceD7mzemAsH2VIAi6fMNZ7oM0KdeNs6dZMqAcJpCS/u6q/Yu2CIm8Dvf86E8hzd3b9YOs0zKxrYVnD6QpnxyFwFqQnXY8y6V7g7EKgO2kexXEBsnWqzueGw5uqe6nNZyFgvc9RO1VOwPJR/25hQlqAtDJ4NTKRGawmEka37XJr7zs+aCPlG3yb+VkImi4N+Um7popRUiOHMRonGbyu8XBD1JrF6hiM2C768sMN1c4bFoJ6oE5Ddm/co50pgWRS2jJY4RE3ckPSP8gRsNf3D3mXo/Hv9qZ1kRRt9vqsUEy6EtqKrjK0WBX6OrCg6gCbuJHycSInXVgHVNIJCtL7nd8U5DMDn2QsHyvcUI6Cdl9AuWF906vhNULTyb/xfkev0Zl+m1sn3YS8ENavb9H2Ib6RRSRsxaKa6E7m+QF1ZqSD0dQNdqQCpWY7JAU8V662c3nxcHiJjKjZi3xSAbhUyOWi1+2wj3/GYaTIoyiry/UCAYWSWmZwUpkncidLPILWz/k9CywvkRlS7P3LCpqNNj9r5KQSOIjczzP22hiRjeNvNWBUtT0RrwI6MdJVcz1FgOlmxZc+eoe1Mc7LhNIY5zcfR+4lDkcqbD5sUYuhZ1p8uQ1TaAMay7RKCD1v1mTzE6OXbDBpsbgTFIbcG9Xu0Dp6QBYRjYiFSwepESCLpCQcQfe3VLEBcEkwvuhcbg7liq/fnksGDWLMvua8hQaRyUCZMSaO66IqmnMkQrItjQsCigS2c0+GSCpfte2H9nVTByj0Mw+6VylLUA2xEJAOhGaGv+UuhBu1VKz38jsmkgIQntOoDQUgVIdVYDFpLyLYhgHoQ56tMrw1laC5LNuTM+kk981xrNN5yUi1BhQtLh6Lf6PqHoArEcRrbgTFnl0VpKXjuV7AKN6N1ze7PRTmkV5pGwNCotRTXhD5Lhd9TFc1CFx0UnLVH713jWRgre6jXT0RYBAOgCCkiFGeHQqspwl0VLKB13VEHQXcAEwQFsxvCNOTNmtLV1OMtnoS3B9tbP15N6Vx7/IiUcWft0bK1aw9qewbjwBgEeVZGIspNO6MhvydCrRh8yg2hKYGhBeEJ8zX2iv6AXhrdEkMmiZlllZvBd421x5HqdZ2Hlag2+A06bgGjxQsmndFdW806XWTnqdtpwlcT/EZzbey0vjdY862kzhcJMM4AZtIfkNiKQKWhsSEtAK16PNLueFmWnAuE55kRmvGxiO9BzkwWpq64og1J3WoWYiA2YzBuMHtncJEAGs5CXJSY+9z1+HocZydCdnQa/HGe5sSYKQI8fk+Z5SPDkFqiPeywpzVgr181k2ar5bnsfttVjOkCud9ziqjkgxyl2DIeRdfmTMgh94JwfK5ur66cXpxfwJ6Ltgv2/adMCLhyDrr09ZWIEf2maIdDtpqRvmgEXh+sn2lGJxo844LFCUpw/38jpBFqgKiprUo5nF5VMSLdslFE9B06nDbxTaMxKivDih3E6gI5k95+HwLg0TL2hPF+3zB7hDYRlVJmw86k8z18hKp+sLsbQyuvftuaaCkHtFkYf30sFP4NgPVDhnNG5j5n5ogPw7MH5+kRRk2c1LMfb1RT4hXwfRW/18LWdEpo76o59x1Af0eCZEkDo0xV3ouqjjfjhnrR7PWCgkC+6YqOLzVjrbzG6D9JkBCOLxpOP6W6o154zlhRn014/LJTXyWquqb5W+JSgltjhRwIzBEkskKJwk6Jy4rvOEbEoOmBHqlWnwkusHVaZuXGMe/4zAIgxBGaq3hHPoewOcVdDaphzoBk+kGMgOno7ZgWBuyeatUdrmZJqDLgunTJ2TbKOPZOtEguXYhR/4qnLKijUHFYBYnOOCjI/huVoNysXlDElF+O2WUG2Wn5UsFlxJRWcBO40Y+q0MC6WuMSka+Z20DXwmHecXvvnmDb1zu8enjDdY1hTKKEFBuJ1w/Ufj+8Lpg/tZZ14VBdkQEOU3xPCOH4vNDem2UEJDYCq99vL1DgFTgajD2oMZCosW6/aa0trGdstVQSkR56/2Eh9+TsbwiTA+C229U5Kem7NyTQXQNmF/b9VlkKkSGYGj+ppw0ZeHyU1SKai6e7ZoqQvnc4Wi51ehXGbna1oQvGYKhvo8IIc68nbWWVrD5aQ5E9ERLgNcsKrlD7HtSl4MTwfRQkKw7Na8Z+axolkdpbGonDnt7XpgqtCnjKpD1d4KRArrOlYflri83FOQCY6jZ4SOZVHiRFy2iJWaIKW732FwCDngWveyPyFcBwQAEttFGUiiAxPJoTu01D4UjUYuuarC5YYLLivQmjBSqFPurIxm8FoKyhaCTJdn3xDgOBgVA7x8D+1ttsem3mXu0AHTar8GDbSKUI5vnbxuAaA6ALdx1x2J6aEiXopGj9XyinFD4oLk6wsbzypF/2w2/53WAqJNxuSfAoAxjQIo9LyJjUNr9i0eQLxooBBSmOZrtnKBx83ZvfR0cjcSgQYJJXMFCJFqEvDdCvy6hw9ivZfh+BsAp5kcUeO+hOYuS6oHRmhaSansHe3794RsjcmATbsgDgyNgkRjAQS6i0sCZ1XN2B4gbTmnBzCqa2yr3vogGaRYTw50eLZp1qK1pTm+8By+tCMVwj4SdvFKkow8eSTG0hGIDIQ73J7tnzuboDH2eFCnQQt3lIzU8p29RXFtL1CG+qwS6o9wZhfLF/I1mRlXruWqQk7g2tKKF1N5iXlVAJhMvcETFIv9xb3ohGicRNI9YhxSCcB+3UUIsxtSeTQOQgjwmIDTwRcCsjOUoWPZ9ZFhPACyXarlDl7j6HcHus0I8nXQEIaUVxK07Xmzso0332NrA54LZCRcikOOk8KAYjXPcAOx8Wkho0ZZ7LP5do0BmGgrjAGO8qRSRK2cDiPdTbUjX3Q2y9iPaFIE63HOuAAPlmNToJYacOz4VBI4paR+jaei91WzTqONg2ecYWO8ncFEhXL9WsXM5E9E9Q77W7jnb+HJpSOeKwNYtSmkTNpNZ1wYFZLtvuhaRjG2q5QTwiQG+Qw4Y0DzogR3n1f7CFD14UI3u/cU7oJmatH2mzRyGKlqHVDGozpLna93V2/QoOTY/ny+WbNf3MeTmCGd4RrGnQ8hAp6g3QI6z1vK1pvBz9Ui7Yt/pdqPm7fPQWnyXY4r3cCiVKLkBQFeMEGM07mEiMlUSUshyVDLfq/gTk6q/kzoH4TtVQT7rOeY3CZ9+4xV+jX4Aj5cZ59dHYGGcrtqYEe3QKeQiSkD43pMy0q5VRX2Zsd7PKLe9vMRbQ6QrhROjeRUJgwWDgkMlBuYAOrT3vuJke55BxkBfLwrL+/ci6vF8f1ASkm3S0g2X54g9quBqBA+D6nVf4ci1g8QUxNWDTNeqxbXjpZ5NiWWfj42iaIQeIcxpTavE/uQpAIWQ9brG2klt+QNINcfadBEJloe6JMyWe9o4uQYfPutmnUT3698JxIk2q0ghsQlRiqC6QZq6wnIz0oK6bx0OS2/OSO8uALMW9d0eVA6l2AQm6pX7QDd6qD2Z6rAeEeQ4obw6qlEsrdNYxTatVUBPV6Sqmnrt7oB6mjuhwttR2IbXDhnr3QTJjPxYkB4Wzaucm0q2ZEadjignvb/pwSMG6o3UDioUWY6d0OH9ZtLikx8xYVsiXL6o9V2HNxWH80Vhilm7ykbremuRkh8W0MMZQVvOyoic3i7IT8501HOX2wwx5qIbSo3qRCWDfONoDbCeSrxqPdd6Q1hvdaO9fMKgOsG1xKgJ8lUwv1UWUrlJOH8xoc3A8dsN0xslgpSPDrh+MiE6LNsGwh7tte7h8dKQHq7gd+ftpPMIKHFPpgNd2QFQg2MQsdwc0G7mzj614lAZaqfooixKmTLa/RHtoIW9/Ja7wv4LqvMOWcY8tVq6NjPKrUag9cSglkBF4eP8sOrmlXqULDOjHAY4CzamD4uupdIQ7WMcCveIyaJrNGv1Mno9pWF6syA/MiAz1v/XjNe/9UVwBU4XFR5NV+DyMWG5y+DVpHQEWE+Ey8f6jOdHwfSgsPN6q+iH9o2C5u4rMD0S8lUJJa7/CCC6MmsdlTYzzYB1INB9wCF3dy4j7zvUHvG1ozIAkK4J+UmhMF70WspJm0E6cuD0cSFTBD8g8sGac9ZIi89rTzG0BqRkxAtG4obJxpJaQzKnSLUIe+rCywholHyyfWAsbnd2aLqqIaoHhMwSFwBF14MkpZWLGS0VsRiMiuXPCMD09or8NHSItvsud5M+K+7j4OobxFBy2Oc4PmgjJT6ZBGrVQWrxfa2MxsW9ev/d2FC0WiPEY7baBe6qyRs4ZRhQ16rbXw/3/AeJvsf1A0PbzuAfAgBSXTPAcP7SrKi0Q3nNoiBeWPMcxkwaE78aUlOw0QDApXG02Rk2HYwj+VnFik7757z+Qf9/8K4dRhqdIi9Utf4+kk1Cgqy2zGEOF/wcIlmXhfHGlWH0TUcumrLZR0LoEgoleg5Jk8t6svxIGkknZy5q9Kh1bkobrrP1Q+Ie0dHFSSrozowTUbx3ERDR0Pahy/Zf+//YJEllpjxHQGgmnksQsG4qIhoxmaFTKDVpYS+bi7tTeYhjB+eJw8FjbYoAyeYMrS0YYQQfh6Fkwgw3GQkmRHfH6GmIdLf3qpFx0J9FowSpDfmcMT2oYobLY3k0oYogQBZRpXZR0owzPpXYo+Penx+sr5VGyz7HGTLkuSiYlTo/Byh0PJouCCLqMPE4T0WevR55XJszm41YEKQBARDyRhZJic1dH+fRQFFTUs+z2iigpxEAM1LaTVsOkxKcHMqMi6SNgfL5E2iBO93+zKXP/yBYMEG4j2kfmGEerGJIEYZ9hEHtZfOyL/D/TscHbaTO35PRWlIe/1U9DOf/RxEeYBuORNGqvsHgl4QuyAmbHKdJVSnek4Mi6+5K+2hrMASxOERhJkxZN0rzvCWxQncW6vd6DgubPY8y5FS0+LJCJWN0wbNDKgDKKaF9+S5oq153db3X1tGqhGye3IAbj3i0hvn6ezkSyp1GekpP3s6qJtDajusULxAqhJMqQQ+MSgBoQUhQjzmf1TCkc+neX1Im5shQA5wdBItKbRMwb1qNOKxQumBKhMOs9z49GquSGelacXjTPbvYwC9VoS3pLDaqogSEj+917EeSgC1+qgKstliZIQdNaBNzwGebwtNnhszmz2nWn5Qwtg2Rw6Tz0kVV97DU0F4mCnKNvODQV7p6YbbmNmIOi6EPRcDXFqQRd8x40cg+lE0GpyiMUtJ5rbJAE9ZXCpfPtYEWWG5K7yWuwSEeT9Rbsp4EKOSQEVnEIX0jtyOtgHmlVkxqLDKbx2OE7M4SnJRkKunkEmkjk2+HlqjTpM6kF00rWcHyyqkrM6i0GAGzRlaSB6eQ+/zFGX2tWVmIkx/imafB0RyOTclH5MkHlKc1dYyHJpVizkU4ljYuPv9bBsqN1i1SU2ZekJxsnSoc2WvzwOpchcO2d9Jcw5F1vPMTNmiKnxueUvkcxwdtpB5/F0HOrIn3KsBk6sIjjuwLsTRLhhsJAtRzBIO3K4lQb7r870vim7ywMqZq6+KjvNuQYF6YQSs4QIuDgY5116pFr2vVxLgXHR4QbkbLFNgwraYZ53I9iZWZ+LagTYzrRwnLHW+opD4R2wzV4ipiOTyEx+fyRDpx9b1ChPWGwTWHd7VXPxbWmiI6zbZhF2CtQFLZonKTNrRllxWiZkWC31J2I117fkCmHEWNWv+m985FtOC4wRaNetX1I1VVTmfNofDDgslUx1vinktIBL4WHJ5MF889TLE6lbG7MpHqwd0f0BwGq52SG0oca9V+YoDmgYx1xZmDCt8MboqjyRA5ijI+7w5RROpjJUxB4+dL0rlRd4vao1eHWq2TMolYwt5YWI/rsIn061DjUcGPV73/KaMdtbCXLgX0dHkuvzXA2zhMaMdZZaNeTbh+nKz1RwI7saJoc05eZut7hKgTesbyOULzLU2Ly/OT9NwpYJEZogutS/UoZIvIl4qJKCtUpaSDBmhNGes9o2yZlF60HXD2WnteUUz/kHS/qAe29hemLC49qmveXHIgEQDqYOWzGoxyQ6gnhGMQjkZE7AwvrI0xcuM0PA/xfYyok3P8fRa5BzpjEFMUozOi+eV6bw0vi/bH48gH6/PixRwM6Ww9IdYO1ObQeTlE1B7CPndRybqN3iM7GWqfhH/5+KCNVJugrSWASMSH1Sb0JJ9vCjvWi0/Ivcr5KA+ib0B8B/w7mKPeKnDsCKtfvt6IrgB4x12yfjgbIdUm6AoS28PhAO98q5AbQGSV3LMbHY/EMDQsw/a+qL8WHij1+x/bWWi4L88MVWdPYbg3dHhDumL1yBTUtgu1ExKG8XeVb4cc9L4x0OTVaOkG1Z8/IvdHoCUhsXRYlsigyRpEEP2caFHr0NpEmEA2vqq3RmC0YGNuYC0mJWXEmFmdUrFQb4BgRXnRttkoLi8Gy6r4qAQhxVstQHrdE+0jqX2U7wbIog9q6OLGQDhT8Pnjhxe8ssOi9l0v9baCPrsw8na/LuKrRa7DuT0PF/fmc8H+HT1smPFKgHiFwThX0VVDiHpE8NIRUFLTa+isNDzbBwAEPXwTxVjkEKxGN6zkRkgU+vfri2vS8XFYT38MASBEbVg4h2Tzzb9zfxD6M9m8vrv3AXqN30WUoDCSbmgw6AloU4fM+2cRc9G19p59L0Pvn4ZI1FiH6qCY8XIjVW1fYzEn/T0b5e74oI1UKOo6M4wJmG0AxjFdK/jhEnmhjs3qbKesHmhjMlKQ/V1sIxV7qDwYoswQsWSrS8kQwnMf8fqxZiuSzSKa6HbjZnIoktMGtpkeq2LtS4skMDxiE4BK1WdeGNNTQp2TSZWIso0IBoVBoRfbO11bC0nhstW65gLYsKTWE4ErMD80zG/LxgArwcA8u0SQrASBdswhKeSq7i1jmPjotFkAnoT3ZwUAMBUOZ3ClRYucuQrSuSEtDfWQwEWbtM1GY4fBvLxWeAFxfAeg12lGst8IDUnyFoSO9LRuF31zyLjF56PQk9VzJBs7V24op4xy6rCl50bTRVlrY48gaoJ00RO3xGgnhaPzI2M+r5AXNtaAnNdi+ThrXGmFwenJBEmJIAdXxGCFVH1DKQdgSRFZBhXex2WAw/0QJiuUVgHeKGAVg6yS53f656YnCQ++nkwv0SN0cafIhnpS5XBq+trEsAhcYWLthcRY7nR+To8KZY8MTXZYz3LAKSKMZiQf6WQFUph/fXUASCHgmNvD/PFmmQpZ6+taW4ce/fjzGe4N6DB6S+Y8VnNqvE6rNbVsw9EyYflkBt9NSNeK/Oaqz3PIcz+DzqjDtrFfOQznNXXYOedARG5UgMObZgXk5v0ztkXdvhZszsV6P6jQLxowlzUEC0LtRTiM+Puc+f3xQRspXu2naEKYsqk8BMYLQAC+FtDbR4VLpqlreLnBSknFaTNbMt+MVLG6HaMvR7tvICbWGN4qxLiFZKiKqlk8GkvMvG8PaIRUoQI56TMzuAtGS8/vVjuPVpvLJLFhKJRh0Ugi5FPGPBHqpL1r2mSL6SJRtBewSCJUs77lRFjv1ZCkiy543xzKCaBGOLwB8utrh0D88DWVCfVmQpuTQYa6mMuRcPmiQXILkB/1WoKu6+NkC07gGnkOo+jGOT2qIgWXhvS4gi4FU2ZM72a0mZUubiwpbVeAnjB3SIWhZAWzUlvR0tQX0wAFpaspbYwNJ/2+TU7I5xtfTd7mkKMv2HLPuL7iTvMl32yBfNmqFXird6rqYCz3qn93YEJ+l5V1OhyC2jcBh4KYQJcV7H3T7F4kJ+A0aS4zE8pR/+XVirJNnJmX0hP5lvOQKeu8JApGIoiiaLhOjDpRNMlrU18TY6Q9v63IV62fu4rCYlzEco06V+QGgBsyO182uJtXYHqEdpQ+JpRjxvUT04m7apHoyND04lO0BixNC/N9jdlzo7VquxNm1NsJ1y/olpguSQ3VWIowoA0QaI1ZUQPVMqENKEzUGllpgytRBMLA3SjregfoMjjRNs3qDJRjhhAwPSXcNCA9Uc9j+3N34ds09BLz6Myicpfs2oi9+r+xYSlScfj2gvztJ7TDhHqrDGNuLTo3j3Wg7WZW5mQilJukaYIimN6hG9SNWs/Omn+H44M2UoCH2BpdKLutw33fcRgCWpMOCQA95PYwvzWgWshsoeom1HYPTbYFalGvMcILvmgdBmJ7oye/fWNIBO3W+TzEd4hAoxedMNGGvgrIYTDB9gcDzCK6iYckio+ZRzsVKgEWvyuUtull454au4dJG6JHfKedG0DH6Ucviry+BC9DGPZ+JQ8g6pAECG/XI4dnj3iM1obX1Nmw1wyy2+QG/HOWd1QNyOHEDtXZnAuDF9+BzXMOppTVkrQkPbq0++uFlH2u+GciohkPNxjNQkO26wTg4gObMWn+e/eylTVpTk8i7QpcTWvQ562pMmjRLgVsM7a3GBfbqADv49Oj5v4zFn52aMlwDBszMi9eUg8yNgrem4eNHq3b8xgJTJ1QIH3u+t8NJekF8jovdLiGqMTRlCEy3nz/ePi8F5tzu3sfoywdtyEdMJ42CCadGq8KSC/MeY9+fQ47LDu+pQ1r2nRJx3d4pIQ6NH/1w+eQ74tDQBDw/LDe+/xEXKs7/EK7+fye44M2UvUIrInw+Lsm5E/yJlGZlgaxjacdMujVbWw4zkqhywI06/1yWcHhVdt7GtQrN2+ULr6pcWiekeekgCh2BHQDI0Z/qK6rdjpYIz2VWxFrhEZlgnf7dVkXLk1lWnYTpd0dsN5PcH21ZpRxp8Vy1cQzSILpV7Mx9w76ryocI+Cp/KT/pius/bOAH3QcqQLTm8UEViWYPUIUJAexzqheOJ0fFSZNS4Jw0kjqqjUvXCS06FTGh4E59xSgjSnXhumhr6ByTNZ9OUGqGeWlBOwyEgjqzWzGo22FYf0YnYYxoXt76mzKHe1bW39ntJvJmIrD3ywi2DP40iraVI4JLXkeDZtC6ujUajkMVImoKpE6DG1OgMybSI4tnwYjdnjdHpF7y6RsSXNg1AuuIWGkdXSEp4/mqO/JF2XkpYvWulFtqDezdtW1+eyySL0Vg1779Iiou6snZXy6ASvHhOvHSaMl62ckCUos8vyOaBSvdUUEnPr41JkgNIzf2jC/U/hQr9vm7Graj6syFuv9AXQ3g65Vm0TW1ptHWmJLMBlcq+xPAJZCaJHrbFnnqRaVD3Va5iRHDyaIKcro3108NxkRBKLwe71oEfB07hGJHJL+TMkiNekEEUb0fao3Weu2ltIhP3dajzPazRzkhJifTdGm1ASH14y0aMRTDwkg7qQW0muupwy+O/QUg5/HHRcGBKnnTuGRIzobsMqzFIvMGZcv3WB5lVBWBv4DvuPxQRupdhSsRwDEURiXn8Qo1gReNRppcwLdHbcbSBVwqQoBNOi/VZlpOM194dSm0E+pkEUVAChnrUug5zJEPmHIW3j7aylBckK7PWhobPUwCmfZwnfml9fqeEg/FnJaOL/eacHfckdY73SRHD9tOLzWSe9aWXUirHdatNdmYHklaEdtHdDeENJCSGdgfhg2xkX/f3pbMH36hLGOQ70zCykS6aZ9ULUBx6WpNOTHosyxZYpNJi2C/FgjjwggMHk3zAB6HqkqO81hhHoYmjFOSSGPy9rVn62YuB0y6k3WBnyXimRkiWePimgTtSAx6u2MejNpm5HHFXxd+zO0e17vtEA6RIAFYPLW7x4NQTeaq7IMhMWYmvZ1bjRpiLKCJOKMKYnflek49YhAYKQT6ay/2KwYoR6YU7/PZVX19eMMvp1QhVFnxtP3MdZb3QzzxRij7xinpLJJy6uMy8fJOjojBHk37LUKTE8tSgPKTYpxVV0+lREqNx0O8yiDsq7XdBajQQtWy1t6FFEP6BG3rcv5bQUXDljNN0a+VvC1oM0Z5Vbzo9OTduiltYYTKgTNJ+ZumKbX1zBMHjE2627stVvraQvbeWEuG3MxGLNW2yekDtr0ZFDuxaB4y696jrPZvuCbfrpavaDnyBt0HmRGbvaEx4arUCNQ7g8xTo4+pPOq0kpEmEpDfsfIdzPWG82HBnuX1ImtRwYb3voiIYwIyB59DkasdpEAbxQ77rvtkPH0vRnn7yXU6++ASMpJAAA2XsNYrEpe4T9AEGACkehi3kMoIzz3wsb2uY4XQvDwbJ9BhYjrIyYt9HzpeuKe9feetETknrxGY3/usSbKvZ0tcWD4X7EcX4XVmAwV7AFFJpOaMQ/KKtRDFskmKJVmeTP1rl3VeXNfDQG5usnfGCvPDzZ1Gsg34oAzDIKiDoWMXYId0tnWadg52JhPkzNGOOrPQBYZtr3KNW2S8yGn5Nc5bt5N+uDK8LM7AmbaXWOHbs2gskT5wLaXGA8OBG/Hxu/VxmM//xxidI5DSwALdUp16PchDGpzVRc3lhEC9x+f0/v7ivcRtp+FvTb+DO8fobGo5arqUAGIzdjXfMzvoEy/sPZivQ2b7ABNvVjEO27mNhYM6fDqeI9+7ePv4z2N9+/ju9FKxKaIdmy/81kK/WM/qpg3ozExA+wq81vIdRhrt3/7i9/An4hnGc99+P9nn/v/4vigjdT0jjCjJ+PTqv1PqAqmdxXTm4sVs1kozLRR8M6JwN480H+I4J1lA+4x75SyJcmnSdt8+IbozKdSg8oc0jl+WFJT/6hGIF2rdgX13AUhwnKXSsJadswdK1h9yOCZsdynSDCvtwSqaUOxhihxggqQzoT8RL3qP34EdVYjxwXga+vRDuuKkIPV4TCj3mSUk9ZP5XerNm0DupHwlt1VQh/Rq/3bxNqu3Ukp1iBR782Ygcepj1MToJFGZgvHs2pzAmXWtiilWWTaO9wqq1IJBQqVAd60UMZdkBnlbsJ6m7s9Me+4HlKvs/JLrILp7YJ5rzY/vmdVL9d1Ab2D6ZijAIaN3HMuPl0EoCLI5wq59oXdEiOVinReNcpe1ng+0XKDlOggrrbuaAEz5DRr5DsnlFMOmDifBWQ5zNH5KScGz5q3SquYdhsQkZBYHyNB5IlUcqchXeqGoEHNGmvK0CU4oki7v0xY7gDASik8YL/oNbIZgnKj0D4VwWTrrRupppJHawVnzdE5HB8OC1tPKoLOFx9gmyP+nDUHZ6+RwnTTQwOvgnJkrDeMcgPwasKpeunhiCWHAEUdt5YRgsdaJ6bsVydhOVwekfiejh4OkGx1SMe5txSkt4uhHEm7JUDrQHna7Q3mPEvSOZufxCK+pvV1T7tOA0Rdv3PMzRH1dkkECLEhQ0MUZc4TNe2EADDq8vmCgA/bSD0IJgimp670zYtYGF3Ab8+6KR1ntNMEnfyMcpfC8ieiYMl563lVr+4DrN7z4KU628np504hvQ5Fk95O3M9HKvuvv0M3YOsgOlKCvfDYu5FGyOwHaaGldopVLmub9aTlxo2PKTCYskRaOl3WI8vm2lxJN5w6KzwgT0Bam2LepsjsLLZ61Bza8ipjvWF1Ct6tqt0H9KSvQ05EoMVql0iLpH3TwoW2RtgEfRm30ScnIhWIthy5oLOUJtY6FCbQJFH828IAC9jzg5b3i7oyu0YhQKaE68cTzl/QZ5OvEq2z1ZveuqzTQ8P8m4/KFk3ci2lzGph+XcEByBGVRASkbxme6Xaz9uLhZBumdqC2CO9ibFEvPgZ6jnAo7I0+aUvTeTll1NMUWpD1QD1fctVNGWySWKTzoh4IrervWkC7gyz98t1zdpjSBGFlSqBpoNdfBVTJyh8kvidYb8lhvT4eXrw9P/QovJ6SOguPBXwu2zKDKsFQlJK2Hj1DDVZyA+kRp91Hkc7+bN2bcP1CiLbN4IXt2jmut12xjRCDCIKIiHy8/d4aYPVlegEt8aYB5DNyiCMoA/372bEWdRyYADoGFA8mXQcCbEoobNyxqiMwP2prn/S0gHdGSg4T6mmKfJ4jKSoppnsFezeGIrs5bntCFUzvCrgklB0T+n3HB22kACBgvdDR8jC6h+0vfiZwY4+dd3mLIaG+V53w3FLL3NmE0DqMaKqXUvfgBDrp0wA7OfMoiihgE/xzeBdWlxAqxQD8xOrdSHi7QYO1qImr56oMznE0yGa9KyH4piXz0KXWxs5VlKNp2V7PbgetKAwoeCkvNMIrLx671zW5K0AdvH67NmrqzbokljIXNYrbsCh35Ab02+8vG5QTUJEdvPpzQxh9VZA27zYBwMDAtM3QtQoDPmKJ/JWPK4AeZQhC2cILPbeQ9nbsJadt08OsbAyqk7L05gltSqHisacix/mDiThAWeh/1y6s6GNmyXZJhGq5tTQnVfywvGs9MBw1dQPARb+3AdYF0+bnCJuZMU9Xg6Bt43fjFd2Dd8fIkovmiXXYNKnTsf35uiPJI1V6P61FtEC1NVBJneiyR8McJtzBXpsC5vHHiVcvPd+XDi90NwmugHo3ex49+1hcx5QQfcUE2nnaW6oUbEkPm4H1GikE2vDSEbVU8cJgeKUb7peg75eOD9pItUyAwXzJagyiN1AT8y6hEYxBcwoDWYSUCPWYdNN9WoDrohP4MKsXuBojyIkLNvDtoxu8+T9uUU4Uys3UBIc3GTOgm1Xuba7ZazWAnicAdIF4jVVVFp1OGovimkdxfbWIJfevX5hQDloPFZPFH7p5qABAK8I7Yo/SiqAeE5ZXBvm4hybA/E5bb1BpqDcTyt1kiV+NrqhWzEvF/No+Vxvaq5P+/9hB1dlTDkuJgFY3eNuNf+MAWKQEBmiVXtw7HGml+I5mOSSqgvSwdiq4jV+9P2C9m7XI9VwjodsV6vXe8oWMUamGvM6E68eM9W773XVOOH1jBj9wN8JNIO7kEAGnGe2oHYEjSuC9kdkWnLMJrkrWgkhaG+hcQ6G73BlkOkDIG8M0pdh8HObxa4PcasPL+6yirdSfQQi1MtBmQrlBb9Mg2gNouGSs94RV5Qxx+FTnPgi43uump2M3YX5QUsb1I22xQsY45VXAzUgOosXizWSM+OrSV9YGY9HoxWvH9J4pyEYb2MvnWwLQKCKi9LRqX6u1K7m3zEqoyINxEGB+txqjTzp07UvKoilnik6ZtJbqSL3pqp+uAgTfh/wBaw5Lb6JHSdokMcca3DhLeyNhULYQwBN3bcfSdO8a87X7Q0Sjyynh+oUDllcpKP6H1zre00NDPteOouwdxLWCnLU8fM8mj1YFjDY8l6EmdS0A84vO6mcdH7SRkgSVll+UdvrsYAsnPPlrEFJaFD9tBkVEndR1VdjmBGX8OKZaBveOCOV2wtOXGOu9Fb8+KC7NFcgPSdlYQ5EvmEDreOH9+np/Kts8ffHtvL5+T4x6zFjuFGpw0kR4bLbgxuRqOjfMbxZjwykNmW9mCB3RDhy0YodJ6VIsl3PE9SPdBOc3RdULHPNfViAx2t0R9XZSeOpaw71ySMDldWgtoHLYLOZnh+cMRlmdvVcGhEcXag+Wl+LzqhJHw+JuNxPaQQucqTLSxeaEedjCUI2xtUOx1HxDBq4fy+Z6eVUjso8eNXozZYfjFJGuq5E/K6CMz3XDSOaU1JmRTXcN1wVkeYDmEWEUaXLkCduseo5ImgfSiAmR45Ckyh91Hi7anoVDvqo7h1CbTwtBdnI66y2wfGzQ15NFcaQKEssrgKrXuyUz9NqpNl1hzFH11LM5TcIJ663WI6UFmN+pmkh+qsjvdOMN9icry9MjS31WGkUIPFodjJWYIsqi5J9wYJhQjxRq/w4tyyMNslm5O3q+JkUs31XAx6z3sqI7ibb2RiKJ/3/z50fordWh0ao6itbgMFIOeHYIehTWLOdEifSaatoWzL7wYaoCmYDlPuH8Pbrup0eFUnkVLV62wmVqz40UTAdx0z8N0PIRY+eSwFIlFnmGDJixpKfPuMb3HB+0kQqpnKWBTeE5IC7RsFap397ArH+WRDcGaTppIq/guP5IpHBvID6MlzdaGeAFy00A2GDIbbwOi/wiqR+RkC8yBmTq1wLAC/n0ZBqea76pJ5jHOpx8HWA5AKG3BUTRLmCMLSZwsY3PDIE3R1NmnvRQfjAorhyhBch27iqAtM62eukg6pEiS0SZsVDb1kAFLON9nAwDb2TRpytAD5/x2hn/f2d5OgGDWrPeWhLQjye/8xO2LU5gahxOn3fSDJGRRbpiOJUGJu4N5rgbK8LWUIUBgpj+HaEl1ujA6tGcYg1AoeR50g2g6USgxjGX9D2At3uI794ZR2BAXHyDrZbOaP31XrBtRnWl+GBs9E2JBVzUIDnrLl313jQy6orauiHb76tuaP63yJdYh2T9xeZ1EajyuQQRCkBP5IvYutlFAWBIaIIZmuJGmn3dymZedF2+odjczyeeA++OjUNYIYhrbEN9HhT9r1jI9B4RObkg0xD3qM0v15+bwdkAomTFm36+qPnXukyXE0LQBPnakJ+czCLxXNSBpO4EvRDxhIHizu4d66R8vCKdMZI0PE9dXJn/8xmsD9pInX6r4XStmL99USXr4WjHjHp/7GFo4MEUVp6vNdTRhRnt3qoHjeEHKKSy2aZo6HBrk9N/0qLN9WitoZcW54NHF5r0j8hjJGsAxlJL/boxema+MLXJIRfSgsYH/f75oWF6qJvkqi8+V+WgwkFT1y6fTfXlbtnIFITZCBwAML8uBpsUjVR8DA8ZkhRWqocUhoiSNj3k81WjGid+7BcRQQueUzICSlfMSGerXdu0L0lbMsCyAiuDa1MJIIvWxqaWAMDnFdOb3JPGRAMUqrtJfncFL1Xrz+61BipfBff/vfb7sk0oP1Wkt1c9f8raYiOzRagXuw6FTGVR9imXFGr27k27CrjmBoCaodCaKBwlREjnWVNcmRVutbxKvT+AZNa5dtZoQ6WxdIzKyboxG2znkVIwz2wjpapGsVh+kkQhOTdGAQlmRL0PF+D4mxRzf/nI1MAvgvlBnZnD64rp3Yo2J3DJWB+UZDO/1ZYorvkYTogZaV6NkVlFI4W7SZ3Jpam0mc+PixqNekpY77N+/1UbXjai0FKMnJV0RxBQh+bw6RXej67NynpT1mQNB43WqvvCzaSCAE2QSlNFjtJweFvBNWld0azjzKs6hlFYfNFI72HOWO8AyUB+VGeASHUnldIvEYVyEUwPpjZDUG1Eq1lLD2tXV3HEZTfnASjsdinBUvTIiK+C09cvOHzLHNE2GnytRSRXm/A0h+9fThSz+kiZuwMV4xx7HuDiwxAxZEpfS08L+Mr/61TQf/VXfxV//+//ffz6r/86/uf//J/4V//qX+HHfuzH4u8igr/1t/4W/uk//ad4/fo1/ugf/aP4J//kn+AHf/AH4z2ffvop/upf/av4N//m34CZ8ef//J/HP/pH/wh3d3cvfOP7j/ldxXRdwQ/XyHsA0IGa71Q/beLoIuuHF7lpz5wVPdk8hRfs7bw9r9U/PMBRsv2hCjVOa3nGdFPVdINH3CtrQ/gMmCEdVLGpwzU9z0bheXlHWa9rOrxeAyIBEJPC8zYQ34w82jFv8Si6WCYCwODV8nTninwxPbxr7Q0ATdVBkra5dq+dKoNIcw5YC+h83RrqXVTUoSvq7xPpDkfI/gCY+rVTbaq04OUDqy0EX6yjQVwL0nmFLF0VI0JZ25D4CaBrhdgiLSdGWgSHTxekh+sQ8VoU58WzSTs6S+I+v+w9tJQo6M5wY6QkAhBAyb1rCedAkkbGgBoRmRlSVLfNc5aqEpEMNtJ5quxIAlIFagLJSQkOSYtGg403EouqRPGpRxOa/8EmwvCcVZ31tfyorFoSoJwo8k3TA3TTXgTzmwXpcQmdQI2WlI1HizLBIvdDhGybMNcezdSZQ9Wj55mHgulJHYr1LvV6yCIhHeR5yi6I2g0xLxX8Tp+rzFnbysPWbsw5M2hTBnhGm9QhiujAC3EFVmSuKInWbilNna8N+UkfKH9fRj0K2gTw4jkc/d3V/B39SAuQTWor8j2eObiW3sF7qFN8iQyEorAd+b7FDJKG9Okj0lgEbBC1TCdVvODW57q/x8k7zq7Nqlah4s8NtKeTj+Qp8hyy7b3XCpYCqSs+z8Hf+S3b4/HxEX/gD/wB/ON//I9f/Pvf+3t/D7/wC7+AX/qlX8LXvvY13N7e4k/+yT+Jy+US7/kLf+Ev4L/8l/+Cf/fv/h3+7b/9t/jVX/1V/MRP/MR3eymosw3UIUdtiOShcZxHEm38/17sF+HtsEECDvnoJND+RnM0d1OoSBdzWnvC2/MYki0qmLI+eK8vcshRoAsx1AkMSrH6IFfPdkPqkAiaitw2z62NBtKONqnaQjNGnjZ2Gwy3I5Dmkcsw+cLYub4ZwSSXDP5LwxgNeHTvUPoZk9SLpq3KX4hQT4zle2+xfOke9aMTXLNwU3ToB+9+B7Y5odE5een9/qzh0JdBD/MEzFM8H6XQCvKTitkG9TFgTQ5q/Ya5uIMlR9VnVd1o8ZOuWtw8wkOhlWiwY74YHGVjqvOtdarvOMb2b0Te5MxCdKjIh8NfYxVzXe7UyOh99CgL0GvJT+rRTw+C6VENlNLVsYGaPc/k3XaD1CAaGaWLjec4R5z2XZuqe19qVyGJuYNtAWoDok0I02buew2elxmw1xoGTA7z7I0Ze8wm2KyKHLoW+MWfkB8zp6SdJhWFBSLyUbFrdRqdAAUArjpPVZCfSNvcX5Ts5Q0T/bl4fyovHQH7fPWIeFgfA3lBEumek5/nSsd58izP5Pfscxro0LztYXrOAdWpfQ/dCPkOxfueX38vQ/G7tDrfdST1oz/6o/jRH/3RF/8mIvj5n/95/I2/8TfwZ//snwUA/PN//s/xpS99Cf/6X/9r/PiP/zj+63/9r/jlX/5l/If/8B/wh//wHwYA/OIv/iL+9J/+0/gH/+Af4Ctf+crnvpbLFxJoyUjXoxblehEpoNpva4M096h2+KfYAnZyQ7MXAZ0cIICTTXyrpTqvVqmtCzeom6thxAy04wSas0YvI3PPqru5NG0M6Lg3AJkz1leHYEf5AkOV8NDrKaMceiHgpjeNeb7lxGjTBCqCw6cNdLnqRDtlkz4hUPXiTkCSqpYDqtkmC3UjxYR6sEi0CqbSQI99DF04VDdOwaYPF2FrmCZTEncYhoDzFzOuH+sCvPsfDfe/IcC1e1YCishz40gYtk2+GPcLYROV2kutqTgpJYVqmYCb6VmTSgiQH3txskfYI1tKSjM2u0VTpSkleTVljtpAVCG0gqgXLYMIyQppJRMWWINEk9QBtJDy+LpieihW71N7ZO/nv9Emg81lfaxL9MZrjToghKEazXadgeVj7cjKV2B+Yw352GE9wvQInH6rIF0b6pFRbjhyXA5ZchXIxSKpJ40gwgGDRnr5zRV5qKsZHRAh6mgGdO2Uu9lYjLBeXhJRo8LdvHFkfL4qi9JygE9F63VcB9OiEcmGSkwMOlqxs/RIRKakYeOwVchkkPbMoEnQ5hkQhSGpmE5gUyhZshruviaU6AEi5Ivg9n/oGLuBAqmzUA9moBZV+/e8dhjHuRN/2pQUcgQ6ojPoM2qqYYcq+TrZ0OttXufUSxZsb4MI2mFSJl/1NkOWB7Z5GERFUqiVfO0OuapozwEoqrBZw+jlGN/h+G3NSf3Gb/wGvv71r+NHfuRH4rWPPvoIP/RDP4Rf+7Vfw4//+I/j137t1/Dxxx+HgQKAH/mRHwEz42tf+xr+3J/7c8/Oe71ecb12/PLt27cAgPWGMNlmqhI+24ho7On03uR9tEDuXpegLyrtgEvgxZg/UJhCxR+7coMaPYMNBWgHZVgB6n26d0nroCRuUUlAQSeGLOaZ2DV7rUw7JoOHbAJIPycwRD6ZQpgStfYaGyM3qOdTIW0yD9yGYZUebrjHDVsIiZDH6MQiMK3It7ElALZ5bjfMbqycki+kLUAu36NV+NMD4TYzksN2dmhZ0xY26I7EcD0jFj5e4/h3H3LbMFsmCG83KmqiMN15MShPDcJYXc8AZGVQ0u9U75Swqa6XvlGLiHbvNbhTOzoz6CZbtEEBuynEqpqBm0Mk8nvkEk77ex0NOWwuusNgz5LMYKkWomC9E+RHwvxG65ZagsGQupHOrxek84p6mpCuStkuNwnlZM/GPH5q0OiwaJ1RrLUq4FU3TElJW4UE7ZuULHNtoKsK2fpG69G2DCSfDXlo82z9uSJgdGoKKWEa9DPJSDDGeIxptOqGS9bbicbGkAC8Vbxk2xcmPRkVIJeq9wxlC0u1EhfTNmwJZhg1Qj+862wV73IbXXyNZBEEJenPsHcXkD4XR3adbfoOiT6bG8BWe9N/ol7OHHEZIykjaF0BcNEUgcGgKmxgrZEAY+6ZE5EQ6YRYdyPoQe54bOfrZx2/rUbq61//OgDgS1/60ub1L33pS/G3r3/96/i+7/u+7UXkjC984Qvxnv3xd//u38Xf/tt/+9nr8zvBtEr3XMabF/VEZTcOHtXoL73epr6aUWNDQv97Mo8vE4AZ1EQpvqm7p6oppwulmodWjtr8i0RzDikrfs0LB06uihISopLNKvO1QhwQGarRJzamlhvFXtsUunamJ+cT3cN5mazlNQF8fwBblKdQC+K7A07YBRjP6LBe7JvUGNdp+wHJpEXAdTZVBodOuoFVUU63hIa9562RIpJozzA2aqN50oUl0skSRCpVtdNi1M63Blc2lezpVOEO70omy3UkUMl9MTmDavRIE0FcZFf6vJN56nNwjPScMVYagALUhHyucOIEF0LKhHw15ZM9dDrmU03RgZLNuUOO6xbr41SPHFGURz5kMC9XQIognfU5T4+Ew9uG+V1DnUl7ASUXGW6mOqKCo12hwp1ABNybzzXIEADMU5dueNzDJug8sJ5h5AbdkAVdS7bZG2wmREFc8DENONzXwEZVnmIsIkdFOu/ErtkNid6IrgEp1J/7Zx22mUfTSSEk8fHWEoLO4EUvYN3nbVjABzJZJUdlHPLs1+XrXuF6HScBm8Cr3cJ46gH2juhlytrcFdjWgEk/H1WLkGw9wlrEU5sgpUbU5fvmpi2Rn8/r1YbnJPvuvft/v8PxQbD7/vpf/+v4a3/tr8Xvb9++xe/9vb8Xt/9zwSyq8OtFpGObBVqNjWaFjgDAT4sm9AHVN5sy2s2M8/fOePoeg+fcYQt1Y9Eam7sEroJy4CiAHCOpOhPwSjHuclK1ZJUlEvCqDLi0JLB3mH0q4KWizQn1SCgHtU45NUglnYTWDkATyWQsH0Ey2m4QNpoY9l8R7SmsjqacPMHMqEdNNKdrw/T2ClquCmkcp8D1vcZmc3CffNSatgmYM9bbjOtHCVwR+RYAoNsZNKf+Waih9aiFV6XMkylry8RoLW0neOt0dD2pjkk9qMozL1Wlr64LZMqoH50CvoyPCIJIQbUhP1gdF3corx30nj1X6bVrAdX50aAe5qwN3rTYu6hC/pQhdyddpANxha+rtoQBgLqofWTCtFbko8KgoR7fVOtwLGAm917dE15WpHe2qU4J9U7HotxmlBvtylxnNQZaC6iwnmpFeuE54fBtzZEc3gju//sV+dtn1NsDrt9zQD0QprcFdF5BywpJJ9WInAjzY8P8pigsvZhRBTBK7YDR9egc4iPA82Wez0BpXcBYxFrd6N90s7SpkAnlTh1ElVwqIBpyIhaFRINBI2K0pGvGnVYqTSsdisl+Nen1ZR517o/dRhoF9wbHBiRrG3a5P2B9lUP1P53bdqw2hpHNOVRF/XSx6yJlLjZrw+OsUCUn2dqegJZSr7Mci2+djMQcRsNV3APt8QL4p0XnMEkQWeAM4wOBcos6xIAFgf787PAc3bhWnBxGROGs+JzQx/u/wUh9+ctfBgB84xvfwO/6Xb8rXv/GN76BP/gH/2C855vf/Obmc6UUfPrpp/H5/XE4HHA4HJ69nh8WJKYINdVBtklZRdlool6w1/6gVOCirDPtu6Mb1XpirK9s0GzsteeSoBVj3hDQGm0FQ+1wCZ466b+uBgGoMUseAcE2yFXAC0OMwaNq04C3bRhrdhTKG7wp6pHTSA7hVam6MGkjD+dDby05tGChfWkg6yZLiZWinj5j4thi9w0For1zVOMNSqEmu5+Ju8M0LEygG3aXwKHm8I7DEDCYp22iL8CjPmfpAexfktiMfdq8X+HNZveL3s100oRwSNVwj5ilMrzY91kzwwZIypEMH5lgyoyi7j1XUWgQGFhjohCsQ4VEoKHr776HFYCoC9P7aZC1gApF9NSSRk/rDW1rogjBzvOa3C4Cq0y06UmQ3i7gN49AA9K99kFXxqh1KGY1fHUC8KjOCBUtt4hWJo5KMCA8iJAOLVh8Qng900bYOep9dE5r9GpjToBMFm0sOrZiSEInRXWHJMgx1NeSzvlORnLlCHKh2f31ve/wZ+vrzjsj+x50O2s/pkmNQPZ7KbY2Hf4igmQBrxneA84L6r1EIu6DEc9UpahaELFivo3XaOPbOwCYkbKccMsclP9E1A1J0eBJvy+jJQY3aGNVzyk6UtGq9jPzPOGeyOQTrolh5Ls57Y7L5zh+W43UD/zAD+DLX/4yfuVXfiWM0tu3b/G1r30NP/mTPwkA+CN/5I/g9evX+PVf/3X8oT/0hwAA//7f/3u01vBDP/RD3/V3ChNkHjamF7Drz5x0Du+ZMRg+Bm/5jAQ1JkNB36hbVmedKEoJp6FIz77Cqb5iZtSSweUmgY7aF8qbuW30rMiT1HpB2vzMCyCls9bEPUrpi5/tBiwsT8Yo0w1GkC91M3FcYp+KQYAjo2cf0cA2iqr9oebstTLKQGOvg9lVzwsBMCOi77GNZ4BsW9C0qZcOuEH1iEgArLYxuGEbcPYNBZ2gxtlSR0FJ5r6QeKkRlbKdV6OqnqwPhqYZSSSCrNguPisRCJjQn4VPKhpgD3eYAJAMEeQ4Af1IDGncyT9u3IpSubkpjOwFyR5dgIDpSR0mrlpHl84K302Tbjr5sUsvgaEbeLbIwHKKOl+cuSam0sJoPLDcStvBkk31FQdo3bUuyTe2DCUdADEeKsnF0W/NjV08d1/jMd/1PsvpefQfORZBFHT7d7mDwWsDqAbN31msmotWGny5TSjHHkGQEBKJpqf2kb5YfRSjCw3U3thwPBz5qTPpI71PXfLJvoupO388OkwN4LWqMr05aADANOS8X2DRiUXuGmHaXDRDEtCcfVZY//WoiYbciefMYOmSMWL26C5VAeqAwPi17wuMv8PxXRuph4cH/Lf/9t/i99/4jd/Af/7P/xlf+MIX8P3f//346Z/+afzcz/0cfvAHfxA/8AM/gJ/92Z/FV77ylail+n2/7/fhT/2pP4X/8//8P/FLv/RLWNcVP/VTP4Uf//Ef/66YfQDUwAyimby0KASN47MM1JhA9IfifxKNXooXflYKyqh2/9T31ElTL0KkXW3NANWZUE/QBVLUawkDJGp86i1HDUvQytF/b04Bt2vIV70xlYFq4dUBCHw8FoMrU5AWAYL1uvPDGnmP2OQBeLKfKrYLD1BPcBlUt31SrxXzmyVqQWKsm/R2EqPTgAPKbYYIRZQqpN1J06UApaHeZKz3Kequ1Lt0KNFyFjs6tmQrCHZvky2Slr4REAtwBVS/TWJxKe23Ame/VwTrMiKlRKh3M+opWU7Abjf5xvU8+tkrXwCIivv4TLEoxOrPnBa/Zx1Ssefpz9mLLHOy/KIgn/s8jzYLDk9aPo5cv248tyEOkt15aJBFv0eYQVlJSdO7quPYtIYJQK/fEYTmm5MWXF7Ia9GQjTjhnnhmiNCGRETXBfwW4MRoNzMkzbrvm8F1IhGVZgZODVWbCcs9oxzRnU3SqMs7QQOIYml9zrZW6oq0+Pg7G9Dbtyh8up5M7RwakUGA2RwcWsvGUXLYnRohn5t2Yxi0OLeDr0rz5WT/f9Riey7K8sxPFa0RcgJaVW3DTgpTyJ2I0E4T1tvJnA7rLuBOhke3AWuo411OjJSsvYaTf4g8XRtwuBQ1ULxo81EqPNQ4Ag4lumyVpwv82SSHrpv9x9eXCKjum3C9fHzXRuo//sf/iD/+x/94/O65or/4F/8i/tk/+2f4mZ/5GTw+PuInfuIn8Pr1a/zwD/8wfvmXfxnH4zE+8y/+xb/AT/3UT+FP/Ik/EcW8v/ALv/DdXgoAHdtmSXffoDfeq0cVzz743LN58fwOpTrSpY6XvtTUjfPEbDQui4mhv+/DWq/m1yJJj8KAje4XDf/azOmy/92LpMGzGiOITjRwA4aQI9GiRRuDwYjHd++N1H6BjVHWUsDFDIQtcBfUDYMQkI5Rdx12qaRru8BYQwoVOpzK5jxQUieBCkX06PkjvYjdQtwfhI5/DxGOkzI877M5fHzs/M6cpEZdssbPB+BZRBXQ1S6SCjYptv8OG8TesYoCUrJ3BHtOoxfPz7hUDl8L2IqQN9HlcJ+jQoCkAa6pYo0WpcOuTenlEdVbbkksZ0JN0AqHXBRbMj7YYKWqga15uxY9AR83Kloo3RpQp54jATrKsIEIfcCMOT53FENPKmgXMywkvg9vnq86Ub5WssqRsT5rV5HwguiNNqbfRzP8LvX5pcYU0XYH+8iG+oU42xJkQUcWyIJAVpxkRGzf3/T6PBoMIp1FM0FcGDUXd4cq3Ov62kggDftBsEOB7mgaBE9EStwY6rbcYfHyAa892xBRmp3Lnbr/VZHUH/tjfwzyGRs8EeGrX/0qvvrVr773PV/4whfwL//lv/xuv/r54TU9lmsJiRO2CpsVfVaOkFB4/E37r5xXHL9dIdSTeyTa22Z5RSEJg2bR8USoTdBa763jhy+qdBXM0E11fhBtrV2h3tW1aoV9SdbHCRhhiXQ1amsBZGk9jB4Wgrf5EFD3+J0MAPdyqYfe5uUKAbDW65q7aj0XwNgZI8TmpfmPU3hP4yLT9zp+Thop1e5FBwy5asGmcILDNC0B6x2DvnwDiIpfrjcUcIOy34DTb2GjYq5wT+sbP3V9u2DA7RYBX0ssRiUeGPxk8jHPIh9nMg3JYi4qxBkJ7Jwil8bXopRdG89gD06uQ9WZUXKagxEVG/pmXPuGQwCwdBZcMBgnU0uw3GNoRDL174zN1KJIN0yH/MyoS0oYC2GV6Wks0HPR/EXMc2j+T+wzS4s+aBvnx4vsxzqZcZwZkHlSeazEXdX9kFCO+v+h8+bRgbNFRQwlUOKBshy1vKFNQJ7IIHIoicIi54nQ+8d5PskdnalLknEVwODogPKcpHQ2FZbdPTlzsNnrbWIrFEePoq1PHDXSDg5XQQ9h1GEtR+7yaxYdCuNF9hytFfmxaG3ouQSCES1qyPo81QYpjGzjEOryzrz1uZe4l8QkAuhoDq4RhVozg8Sx5sNJdOfTHalEOjS+HmyfUhnF9ziVu+ODYPe974hNILlUEINaUv04AehKAe34IlawOIUlJ9E6jpv/O+HwWpNIPkmvn0woJ60LoWbjalGQd7fVD8AwWgTjz6v1uUK7BL9b4VJMoQl2zJ2NGF6ie/YWbViiX+aMdshDzcRAA/VxMPjTmUNi+LZuOg6NKd7PVEGXNRoOEp5LlIgrZyRCuz2gnnr32sDNh6r+MKaWz6HFzunFndcSQsD1aI5FBi4fEy6fZIPmED/lCNSjarXlJ8Lh2wij57CVJ22FXc9QGVLp3fVZi5VNZJQS2knp265svd08tRXCSIZQbbmK9HDVaJSN7UTa1oUeL/Dmb5HTmyfIcdosSMmMcjdroWc8H4Xa0pOx+7xGj0n7lLWmUQZRiCC3OWtLEJP78ZyNJIYcrQ5rpMJbGwzkhHo7m2Yd4CoBAKLdSpQwJN1U86Vsx5EI/OoY3+msuyDV2LPZqJWMzyBygwy5OUAGwybMWrx+o3N5ehDQueckvbmkOyNCWtdVq0ZT672g3gjao9Y3SlJG49rI1m9W+Hit+uwdjmMOpqU/b6fXp4uiAuzdpMXg8qQkm5jjZSuc6i1TxKIzElXuUFZkBV+VvLIRHGbCcqvRu7My2SWf3JhEdCPg89rzirWvDcX4zRHy+kwGuDTUSwpWYDg04/y80c7DdBKst8qsnN81HH6zBvwccmtuM8nq1AyWB6nRV4eydrSCyAz3CzV/LxwftJHaHAQE1BaCmt2TALCBqQB0j7dqC4sEhEHTHJPtlnbu8Xuiot83h93BTggwuncIavpkIVFn0PMabqTGW3KlAYeDrJGd5ieowxc2aVVRHAEhRKht+aZelDriHugTfn+4mnQz4+MV+4Lw2iW8om6kIhm7h8IG47uRtEneUwjBYHQD5q1IorGhj9NLSIFfq5Me2k4VnYZFzn2uvAgHu3foTDx3IJxEUqquHkoBWXUxU02ug8jw/X1Gn8KxUudHc28iNr7D+549qxEZcCWFZJvTEL1t4SSNhigRUCno6222KAhQeMgMVie6WBQ3RMUxzswddoYbut1DMc9/bEoZn9+Nx+Ye7dlscsQ2n2Jd++uDcQ6nkQExTURJMIV/HR8RRK4Xosl9sTYtseH6EDf0KPHqKiCla1hu2KgYDAcMteh1SmFgbH36XHUofguDSp8f3NUpxKDIeC7D89gIBBiBZ3OIGa9GkNT00t0h3s9/GnL0pM0sSYB2Hu7Xn+9goPTS/ZqGZ7tp97J71p/j+KCNFK26+adL6s3ZbINucwLdHmKDDyhk3LRctiep/Acu0IGfklaei2+UykTbaPUVbCKIXkwLE5m0BKrLHAn04c8ZzZKwmki3BXitEe3EERRn9U696VgdpF5qEmDslAqEh5lM4b1vntypuckgIc9XBFmgbY2WQQp8LkqnRd/IhIB2yr0I2nNIvkh2G6Yyn6oii4stPr9eD+QC2lDDxTNAxo50BQ++4PlYySDMKwl0nFWdYTXJFhHI6YB2MyuFfdYEL5qgnqbnC4eseeDkToREMtiNvhCFByuJIPcnPU/pBAWFpvp9QgRYBdPbBempnzsiZ4egzMHxGhUc5l4svIMxVW5Lto6Q1cnVeyWrQATpOoHXhnpIuHwxY71RRuD80Fvd+Bphh0ydCOFMx8OkCAAPdGSP3qakGzQLxKFYv0ZHBeyeFCpKGOnb/b1AfuqRfbp0UdV2ytuGjha16f0TUhKkJ82ZpCsFy1HQN8vlnlQ+rE1WQGtz/GrwIbCVVNvleilpgbgcFKKM5yZamK+lC33eq/oLdaKNkVLgtUrrNpLyRpn1AFAlAFoGQ02jF84qhUUFeNFDBrb7nEe3FuWPdXiax9pGNK4KQqJwfHNi2KIsQrayGX/2vjbiu3xOrj0fF/qYQCdUfc7jwzZSVnuQvR4jE+pkGzilaM/AZsz2CsfgqYe6xSAfZmNFiS0+9ebZxTSvMhgpmPSJLZSodVA22vRo3pcvQiLUY9bN0b3yKoA0cK3Pu9AOE41q1XxHaqA5hcRLc/YWEBOGV21emEyDLogcCcDERslW3B+VMTaEE7LaGKAbSDTwZdEuxQ7llQqZJ8h0h3bDdi0URYIBSYyekyhLkEWQlqy0ePeCd3O2ZaAdCM06DwsjWGX+vPpEsM0gk45tRHs52i9QbWinCZfvOQ4Qq1paSRQb33g4/TlENL2tthWgUoFuFsxoNwfUV7Pm0K4V6ckMo9eQiN5oQEUPZySHI1PPa4VIqDRQNQ22yURNmdQxuyzPDBWvDelx6XBPqcA8oR4Y14+NuWeGrxwZj1+2pp1nwuk3CdN5gAVhOnIPtokCEOtqW+5mLB/PUPacCseS5RnabDJTxcaG+maHagxRu652yAo7L90wj0d6dwU/LTb3bc4xo97NWG/1e9KlaRdYUbhPLgJAdQfJW9SbkXJD1SW5LGpt9lOBm2823HxTTN5J2ZCOtOgA2o+wFVKrSo0X67qwKo8F4DE3TUoLZlwtt+RIyyY/aNFvnXXsdE3pNdYDI11SH6+I3N5DQvDI3tcsW9TvedmkOqPj0UZjOpM2pcwKO57mFLqCaADB4Xf7/qFMJPQmzTmv3pFCJGjon+f4oI2UH8rqs4T8EII32EP2OTOyreLDfQPtDKy+wGIDdSaR/4xHwCPoEJwvTIe9/PyG2H2ugNehNL9O30d2IfbmUuzLaYyOmNUrfCHsVsq29WkaxyPGwn4foazoEbX1gDv994WQPs4DdKUAMU/x+aHeKYFWWLIa20Ebo7QX4ErxehyHRuw1MR02fT5GPmHqLM4Yx2GcAMDn0Pg9Ae/0thNubDaspuF9kQutBhnGRm6kCpMT2rzf5o7DdiMMGIrn4zEyKr0OEBaNQiNU/+EMRL+p8X4HNtymlYzlDQPW9bm+OzbdlS0a903cC3E3j6ztTzLU5QSEhXh+IgROpM9lHwSb0YkCX+nvcYbeqBjjZIVQdXCP6T37RTB5AR3fNqx7JsBIHPp3P5dtJNTvVcaxHceOYHlvPU/TFJxes+WjCU73s+9wbb1nJ9u+Rs2KhR0a3/xx9+/w+oap/GLapBunzfc27NIvv8PgvnozgVO23ANUzsNkRFydPKRW1hotjMU3b1/IOaHdHbTLJ1GIQkomzG816UoBC+jkcekZ78sD0bC4nNi8I8bsUZTDPyyghTX35R6a6L/an2m3Uw5MuqjhIPR+QiKgRRPuAALSpKoQTXjb1Wmjqn8WenjmVUkd5mViCBuBJA0hukMGQGyeklJ4gyIEYoDdQKJfu09MaqJ4fmOkpxXzOy1eLCdCcb254Ti8ERze6P3Mj63XSllLAHFIkazw91IDAgqK/rDROwYvoJ6/EYTy/Hi46G+bbC4tBJLhPRHh2mvXFfmBe7RjdWWEISqtLaJUyUlbhcSzpg671rpxMoQItCSwFb62g37OxY9dsR44AE2QHgF60uefHwsOk89nPRcXIyM0Qj4LpkdBvuwis0WinfnI2uO1Ij+arI4gVPLTWnofMFM3IDPGIaHjz2It4CfS1ueDMsw2L9WlzDzSl0kVRdYTBzuNF80vLXeM5c7WLg+GylvQjHu19JzLOOdatvWbtbZQ6/KgwrH+Js9jNqis2dXrgHYOYMJg0CgM9Di/tCcTaz3WcOv1oFJWbe7GzE6M9S6By6RRLGDlEO5BY+PM6nXZPKcBnvWziRbkMzW40KznnVXj0XpbPWnU5ZqNI6NPLxibNa8kNbNpVoNF3CKfHBHcrmbvfccHb6SSJKRHo5OeJqWHT25AYF4lepfXUo0lxRsvtdwfsN734XAG3/xWF3Hz5L7BWuWk/58uBH7URVBOhPUOoEY4vCbzlptRTtVIcSKIDFX6PoGsr814hOgqIWrB/Nrcg01XpQePLaRV42ztxAE/9moMrIn9SJ5WRFGsU319oxhl9xUAQo/EiqgBZr2wDQ15VIQQK0RdAWbGNCWrfp968zc7qALHTysO31o3USi50oRIhxUhkFK1ueHKfXMADOoYIgu9ge79C0wmZweXGGxcDwxha3Bn59ti/ebwXFdds0RdcRpmxOqwkYloLvQ4o53MSPlGUrS9iormDpErUbR3kZSUdk7QXmpupI5WRFmMtfZ0BVoDP62YCMrYOiW0g0LX06Npy121PYTnYmJqrJ3sEwamaUPKzP6dCe3A1m0V0cCzHUyI1Ay2wz4hJLsWY5QiDFM4HDGvfE0A1NhYaKZxeVL4K1uH3pa1N9b1Ey/nQEDyaZXoc9XZuC6dpFC+K8O0rAZCa6JIo/y2C+AT9Tl+XsGtbVmMHk07SclJI2gdzreOAP7c1hvaGSkVCGizRb+TGEMxYb1lpCUhWa6SDbLeHL7eGkBjz/ucsCdVUGsqhwTXBuUo2nbZMkUINM8+nj8IMb4WGiAThfMba0VkC/1NSaWZyv+iYt7/Jx0SobbjrMBYAPcs/PXiNXqJzrU9nGruMBONrDPDpsMbs8I798qe5TIdx3Yj5CQOe7Bgg0JGmDHgie75O7uJWp/UYwIypPoHiFDx7AGWc9aaPI/q41pHuG7vIe7hA3EvivBeOHT4fHhzjICBOgGlP0M1HgCvSv3VglMK1pS3m+/XTVsD8gIRQg2bRnvOwAoNtj3cNEIy8Zyt9cOUFbJqAqD2MTJI6n1j9exoeJlZ6PezTzMYdOLnD8bhMH5RR+MwYtNISATgmSGFwNBkPUAhdURFJ0TMKydyeD2aG83B8Ynicr8Gj2qraE53KE8AdPz2c0439GEQYp5iE4VEcelohKo/Q5vMNgXj7+P4DV/s+oW9USSwKagfoX3/++Bo+Vi/uH7cofK803jY5zt0Zn/3tei3blp+VKAlALYmu34lNufYFERjOO84rGQF4RYxbfqtiWohukNHQn284fvKbjyH6x3X+zMYd7wed8jj2p8P30vHB26kYF6MDXoTlW8ZjYXvn1PSflNTBjnMQrqBi2mGjXp1gCYpy1ELbtukDcqijof8PUCbbUMo0KRtQfc6stZ8SDLK67UrJ4eRAhQWYsBrb4RJ4exoY4+45mioaK+1gymb166gLXOGnCbz+JYghdSj1p+kq8M4LWpjFPq0jqWkUUsIiPrkBqDCo+pBMgCIJ/4RbbYjmhDSyEEEcjyg3R9DRqUFyYVi02H3fiu0aNaEQEM4N5F2Mb6bsWEuWjQSas3J68WSQmtNBVEP37oMNHqPdp6vrHbIwCuL8JhU2XoVUM1IdzO4NvC5IL05g0xWSNmS40mgUdT6wmpcC9hzUjn1HM5sZJ4x+vMWCba5eHsIXnQBqx6bQV1QjTw2r5mWgmR1ebxM0XQxnbLB4oJ8LiDbtOuBLa8i2jHA6PZBpllZmXZhDG2NOWMNAJ1X5PNqa9DugRlyMxQQe1SerMVI0vMGOWc4wjOH1hxy0ec9vVtVTotmcMlqaAqQnxA6hsJAO7hh6v/mi1hk6lEcMD82TA+150vtOlwpXeeXGd8sJu20c+LQnVTtzLC9D8AiCZv7vCqUraiIFQJPWv6y3kAV4E8a3eWzztWA2/xrh4jFjdYzZzPZWDOj3Uzag68J8sOixeylIVsnifpqxvWjpHVS7sSJkcKiq7hGs/H9DnEaJBzGfExhOEkAu2v7DseHbaSSLkoxRWttxaCMINf0AwCHrtQo5c4Mc8/INrC8ry+gCcIJbQLqpB00JbT27N8JqIYdz28J+a21oVhawGn1lFFOyRQLGvhpNFLDBBNROuhpVnrqqgW3m3ofIrBBRZKsXfzJ8gIXdOrxMWkPntqQ16r6bEkhmnKj45LfSVeeGOBCL2hMq8FPTSDHGQhj3LQJn22a7Di0s+aKdGIFc1BfkRPK3awbIRA09tGTTYsgX42heB0M6LKqMZgn1Lt7rPcTRnUEWpsy6qpFM6SeO1I3klhW8NUYY57z8ehut2Dc1rREtsD1oVMD+E6Lwed3GUd3OKbcdfJ8bgHAMjiMg0dJlwVYVt28jzPIVRmmQWx2KH4cvd8g4zQBN48iMsrESv+2OigqTb9nLSAmyHUCT8aqu86aN3LKehMTKeWot6Pz0guy/SjWFbkR2LsD27OE12s5ZEkUPb4kM9ppQss81NkJ2pRQTwrrTUzGjGvb52HrCNDOB/nBorerFrHylOANGLlol+B8EdSZLE8F21DV+ZkWwfx2K/pKAuRz0VYpgoBRhQlyYO3MKwg1GAHgNRlh0EaExHOWcQ/W9NMcORfP5SKgR/389G4FnwtkYqTrwTptE5ZbRpvV+eWK55GYzSlVR0/PDRSwUU9Z7yesd4x0FaX3u3zZos9EDporrkfN62frvqzFzdYHbA+X+HdFjsxey30uUxUrBXgP4ec9xwdtpMZDSBP3z7ywCC9JYbXh4YVmWTLZ+yis3Z8DRu0mjLlzAJAK9SpE+yOlBVGpHqrYBpu4ttozAwVsPfmReRP47+DxRz6mQw8js26T29rPpSYdDhF0eaHxWjYfoCjqC0oxGUQzQiANGLXUwAzkF6bXsLh8oooT2izCeVbsu4czNves3jyNsIdbmDEZ/z747TPhwSEaH67fFe0lWYQ+T4NV8/t37EYFgkcdOpWsSTruzNtmct4DSBR2GfNx4714kW40qvRCVBmuY5xb/rtHnVWUEj3Wfll9VEPPH8V6cuctzuHX6Oe3fJP0DTtaNDSfH9INlJMIxjyUkQm0/scK0/cwrF2HNMvV2ZjwCvBiZCnB+zdBG6NYj374NQJ9bJ1ART2a8AJw2qxRANgZKEc/3JAPzq/n0fQX9FIUv0SByRDpa4rKKEEpIDdfO4mAMmg7jsZpnNsjVGyw9zOIexiLkSG5Ofwen43r8/UVER0AUALcgfqcEZQfH7SRIvOatavqYNsZmgC0iSDCqACosUnO2IZwY4w+dgjNNhGbsG3ypmS6AA5vWiQT4xqcmCFGYrg0Iy7USMbnqnVLVAVkdTtx7DdJSzISWRPHgz4iKg2yrMZeM5HYwsAxm6gjbZo7apRRNzUoVAXprDIy+aJFrrSsG8/P1cNJoJHBSSuFZR7qKRJFD6SAfezanWnYbg6gKSs0eF2BWjeTUxmS5lnGuKPXoLm0zpQ0+btujaJvMNF/Cwz2zr472CVyHqOD4gQawPTl9gWNGp3mcwujoOdAwMnlyFg+OYBvJ62Nerxq/mdsNpcsmvDraU3n6alDzqFG4L+TbaJQGCaeLZkXaptnm63j8ggxijtFtatE2Of0+WqpgZSkw+FsSBEAFdlES/lp1edWrHbQVbKbQFzvcaXo50Vr1d5k41xqomzapOdPItvuy+YotTkBWddxvZ01qnbCEdANMSuU6dCbJI6apOO3K9Jq7FfvcWYqJn6EhFMRVW3fMzpJYV5JpO05jEU4vS2YHooaBdsTlFgxGBff8J0kAKBNE+qt1rdxaSAjp7SZsd7pfMtPSlrRAl/SAmEiEwAQyFVZdT7Pta+cP38lazCgjOVdrqk/V1g7eJ3T00PR7zT2pnauHj7HhOmxgYThgr0tmKG297hDhedOfayxicP5cnizy4DtGJefcXzYRqoBRBL5EN1AOu0zBBphuZOm7CiPBNoho9xpYWKXLTHqbXO8GjbJBIdvVxU5HRYZrxV8Xj/Tu4g2Fx6S7wvv9vis9xmaMtpkRRKwPjuwzy8NlFR7zTc4yZ0BCMvPkKlO6PUq8ytfoLDGtfS2D36YNywwAzTrFGlTgsysxpug7TCAzUSjpjCIK1HgmDXPUEyxgHtk611HXZF5PEfAJ26AMmu1/rjZOrEhuWdt/9/ckAHqUkskjbWHkS0su1c9R9rCbFAj5cohXSLGvGDLY9AErLcJfGBMAPJb0fxNEqgChhqodnJHQ+vChEgVL3aU4PHQan0LMT3KsEjDi6brkaOPEq9OhtDb9gJmPdlgYMiip2o50IaAZ7R41ZrZXRegFIjfT85DlGC5RqOWey5wQ50PAoHruDWtxVuGyBYAsuYwGznbzcbqaVW9PrGxzKwJ/UkbPLpkUYMyHed3K/KT/u38xaysvVBDR0SYmluBdtW9botuvdC+TUoLX+5UoHZ6IPDTCmRG8Y13nKMyzCeLUjUPRygnzTdqnkxzTy1r125AkZfocUbWcwtAtM2RIQCaGetdjvIXXT+sxigzaMdaiFYpbGFXA4AGvjTw1fYmy/P6GIRAtpVz1AOjnAYnyvYUYQZJ7RGUb2mjc2bNSVtSsdo6M3hVtjQt/Tq/0/FBGynvpuoMH0GHaDbepU+oYTIA6NCSQwCQDWOKimA6a+g7nVUyhdddMtTzXZa4f2asSD3fMFhrVU3+MQ81vBejR53I4CLt4/K+ZKOzEEOXbw8huoQNmfe3DjmuZyfTieubWb82G2sWrYt4aY7J8LrBcLB7wOB1qREy47LbRFwSadxUuvp12jw7ANGBeN+eZaOAbr2LAHQHYYwwRigtoBoJ2AMkoVYwMpNaAsgMbXK1jUJwlQ6SgZFFFAZHCzqxLa72YWu+0bW+2cectGEJWv8wdg7LJh0P5KQvj8W/wzPebKajxz3OCWaAh8/vizj9dC/AsS/NU40Whr/t3xPP3KBRV9AeUYaAqyXWnFBvFe8NRN3xcchsA19J/67N11vdknbKHp61X5N/v3d69j1lWCcbtm01IkRE9J3ZNsJoUVc05LQCMo3P+XohBMvYUYw0nGx0tMbnYuP5Iuw9wuIAXBYLRSAsoIMWOL8vYorDiGzRHHRWXkBLhDpb7zwilJbAE6OUz2d+Pmgj1SbNJbF1mwVJ1F40Yk3a2YTQ1g3SNykmiyx0I+BrCdKBs+vma8X01lhKtcWkhEEOQubdnLRXFi8V6Vr7wrcaino7oR4TXJPM5e7psiq70JP7ZpAkafJTpqQ1J467e1LSI7KdmoB7+iS01Y9LKfS56Lpqw0YrbH42YUs1Rth2sod3JFroGCxCM0KKo7d4rcOnGbRa8jwlJThIj4AADIl0mDcNUCVMT1B41K/hOBn0hI3StC9w2LOHtTYnEbRjRr1VuaJ0XsHvLjoHRjbeoBTu0aeIapQ1p9c7zGKCpcIAspJpIASuCfNrU1Vfi0JlAHCYAp6hNBjf9oI3OUK0NocAgNDAMNV1QwhC2qlabgZK7qGkLTT4OOtcuawRLYc3LFarVM2gWgQ00sE1SmDQNFnOLG0cgDjXniji/7+BICU2QhJzmLwlxyjp5bkZC03qzRxRmjPlaGnIKIFi0Fohh4xyqzVEdSKsN4Q2a1PNw5seDdfJoxfLuY2ARgLKKeHyhRTCtEGDt+iQiu4jatQRhJPNfQ/3S0vB9Bqa35pZG37aHHL2rxAFqWp6KMH8HXUO2yED2SNpc454mAMtqQ6kQ+rxer9BmZJqVAJRDK9lLS54PZDLmqYn+EqgYza4z/bZIfcoGJwfy/e3U0Y15qh2NHa0hHrg8LH+T11+BxTztom03bERErrXYKGUe/+tT/ReAGoL3CjY/LSALosuoONBRWbXCn66aEV8TpDjrNAQoBp4Fp7Xo+c8hnOK6Axn3TTW+9xrOprCPjTCDWagYJR6MUaUi7dS0ZyLFJt8m0QpFNlyb9c8Y42ISD0bIlAzPHnvpce4qeEO0VSb8OGwJwpWVjRyo9QT5B6FkHujBHIIUhQa8eJQclfXo1h35BmoTGB7wRsnysHyRmNzPvM69YOA15GxiObaWgPmrEWv2fo91WoaZgwchi6jPgSucE5kLEUgktQ8/KThXyKUJ1VZSGvVPmZmGCIq8AjRxuql2ixl45Xe7dWfiXki4uPMCPUI9cglcqeB/x+05IKKwmzPveXWIzYbY2JSHUGHIZ0VyaxODpPluZyp5U7RbmE+i9o8IrB7t7klTr6xcaIxAnanCADbnFPyUjUms/T1DC3CXW9M8mmGFuiegemxIT9WZZTesBGQLKgd5r5AdSLXW9tMfU76rRhURqXZ9aAXqHrLlvEw1qSLBJd0MHiOLLfj3grQZoUVFX419pyLD0wZZAKv8eNsR08R52G8I0eLcF7cALlYMouTcuwztfUeZYCtrV5sXQ+m8LEv1dhHUtTh2GbOwnrTI/4Qjp41x1WvL0fm++ODNlKaO2pB7fZclIC0DfZKEHK83DyXRCD06CT+NjCcPKGudMxJ6cWu8jx4KoCF9NdmuK9GVtI45GQkmYikESpoFFbk7aahZZYNtCqdvpFKXAihqz8kAlb0an0RZUMBOuGz0pAZHUbwe4F/hmwzItt5gS08ZlEaMSt70bW+7JBkm6D/v6m5x9+ZtovbfrRoVBdEulbkJxoo/R3G8OZ00SLCxhnUdJNJbjU69LIxktEZWA0uL1WfibecH3Ir0WXUoLdg2Q21LJ6IdyWTZwX+ovnLeqNJ9zRnkAkXR5Ifw6ZAZplt897onSVS+arBgZDMIcgKq63xuib3ptnqZ5x91TKDiEBL2c6VMWKigRFJLaSaNM+hZQuam30hd+ZOz9qjZzrMkBhbW0+l9rGNQm4ObxxGKCDu14dmBAu8DE2NUbRDXlxUPYMqAaSq4byqE9HmXhrg/ms9JM3juKK/ibiyd8UFwoFqs6rJe1mDuNoDacQfz+4luD/YbW5gdsPYxBQxDLactWyERZ3usVEkNSvLkO25RmbgZj4RmcwZx/oK49uG9TI802csUi/+dwQhJWMKDmxf7A7Rnw2L0W+DjYSRhnH+DscHbaTStSHVivS0aNg/JWuIJqCrBJuGXZYI0EjowBFS89nqkFaLMEQ0YUsEOc4on9yoVp4goIgRN+ZLQbZFU+5mrHc5PCVnsfC14vhu6Rc+TgJfuEXFsig2DQJwBG6yeR+MQhMg1rrgqdf7pEuFd2etN5Z49oLEIqoEbqF1qHL7ZDR4TDX1PIFu8FBiUE7ofW8kaq2q6QcC2BJVzAvlpUXugNaqkU1lsEWB6UzIby1CmNR7FyaU24xyoywkXuq2cWLWhpYNM3Cwe7yuG2p+wFeXBagVTKSTPBHobLVJpqjtLE9kUaknRhgDmSzZeySUA+HyCaPcKMU5P1l7cAFgHIJyJDx9yds/KH2Ymo5Ddk1ByyPo5oxuXL0MwIyjHNRJ8RYZMmcsXziiHgj53DC9tflujtPeaMrEqDdZx9BZdy5LVCooJ1Ow13ovrBzRar07KGturTrHIm9lBJzdnGVP+N/MKE56MMYrlxa1TL1B5BCtrwXUtI4vIlqLlBt6RNBvTFST0grf5TBFY8j81MCLFySb8RbR+saJeimBaOSyfDzDOwbw0gIFObxTKLPOHR68fpRQbo6bS+FVMD1UnedW2K2NKc1CmHEIFlxE317SAUtDKNMQUEitHnT+JCtujtq40pCargmPltts9YzX7ij4OMXvszcTNdYoYFGeQneuYN+jrZHFpCkVzyfxQVMXXFLA8xBR7U83zp7zA1AnoB4RxlGvSaPcloHPB/Z94EaKSwPXYcITQbJtQN6zxNhLAX3N3vAMBs2U8KojD+ADPk+ox4z1PunGc23wDqq06kIkb6kMgE4T6qx1MbLaJl8E6dFyIUza7dUnghsJhzl23hgdnabsmG4y75TBkUcxL7ohVAcAaD5OFH9Plx5RhS6fJzhJYYHAr70IF+ienHcftUMjDMT3A+Y1TdYDyqV2Ijo1YV/pOUNVArdcyGFSlYts7VasPYfT7WM8RCBi9UWVg2nkzzDgs1qVmVYbaFnVS2c2Q2nX0VovMmYrgnU1AGN46eK0SOoElBurhbtSbzUgOk4tA8udFcFWv37g8BbIXrzdaEv1lT4+mn9AqHGo7p2qbbSsLL56IBV+Pa+gy6pt2ae2YekJM+qrWTtVJ0LeRes6z7kXDnveq2r353rMaBNpAeea9CbqjojiTpY/V6vzqkcvf7BnVRjJFVMS6TP2InVXJShqdAhA1IyRlTi8cMRzS4x2tOiMYUou6vnzanT1mbQgNhmjr6jj0BJBJoCEkFiiPk8dQNXJE2IjZimUWI5dFxACpCwB2bH7nyMBZTdWvSHo9s/etwtMqIeMemRIEfDCG3FnJRC1GFuaE0gyWuLe8HB8Nvb/mlsz52CTCqgYa+lCwX/IcwMY4G2NimRiNBp0SUFGMupzupd+wGBpuxzRfJoaKRW3/TzHB22kFGrQxYXMvcU6Aa5GYBV4m7C5b6yssIwovk7e98d/WOsV0sVYcW0Hm0G2eLRYbximKBbsMiLqcofI5NjqYj+xAXhx4bNiUjs2rSDsSEvTnlNALIZQfwB0ozBJpU0XXQDSkhEgZNtO2nNTA/VbNqG+bK5vy+6T7aKJ/IS9iW0jsAjYRTeDvbXHv4cfb1cQ110rZC2bnEanrXP/rB+mSC6s7xMhjRhrA61quJ3BJ2YEeO2bVL+uft/eY4yGDZEXd0BgLE83/AQxvMYjE93os8niNGvHXoO0Q1Y0LplD4SIcDW8eSGLRnTKyNCrMvW4Klph3uSaDTP3fdK0gg0ZHx8bnBNWKsZeVs/V8vjsM1RKDWUAl95Sf+HoY1xC2z9lhrvE17gzaUdU7+hg1jxIEtJHvooC3XK+PqkU0tnPqPmGGeigvcAOl12TnkB4lPpsHjGAj0hgiuGPmv+6DQ7KIx8gwAY9Zfirmu0VegDs2OpY89IKD/x3ouVAAnktLVxu/0p+t2JpXFi5tDMr2QtGREZdS8zVKFOu33GYlsMxq2Kv72a2PY7qqMPfviJxUelpBxwnl/hAP2JvepUsDUAZvvn/ONzItzJstgpo6c2Wt4WWnhwV87USGqMshgKBRGSVjzTQJNmD/LoUEA0JrDbR2Y/giO8hhxxcS7OPkiTyOLdj8sIAfrdHhSDJwnbTMIUs0Muo46wSlKqh+j0ShqA3YRirQTZk1agMsamp9Yaj6hrKp2sTR8VU3RjdSUO97npS9dJpU39Aw7CiSbMOYOMzkz8HqhjBpsTEvFfxmYCu6ZlhOHdYbjVSx6MuNcGZ9/ldoR2MAQlqTIgnROTjul/qGo/I2qgfnHjsv+tymJ5XaAaCaaa5qQq4yAYu+NWIrJ2WozQ8q8UVPCielq+0SImhzBruSts3HvFTgulhUfETUdM0JfMxac7WWDvdc125sPFJtgtQakjtrbkxyz0Fi5V7355sUkzEqq8ru3GlEwEWjlGS5Hz6X7sG/EKFHZEWkoak9d82zqsFh9A2YSusG0DZySWxNGikMmTuN6SKh7ein52tT/T8AbZ47ucGcExo2Z5fu8oLgsWtvqNVUsVpGu64qAI+GGZtD2KKTpASFeiB0B9MV1t1IS6QtVAF9NVRCeg7T15lIh/WrzekzxXMd4eU2FEtvngUG4yy2l11r79RsYgr19oB6k9EmxuWThOWVzuXlFVDudNzSVXOEXID5jUq41c9ZK/VBGykPcwOfTQZ5kSVBr+ZJjZtTg6mNQ/MoHhGwemTukfgnvM2GTAk1TZGHALqhCizYMOPQpLPFMTYHpPKS1gi2G6gfQ9T3ucZjKaCni/4yT0HzjWiI1Ii32VpeV7JEt3YNFfO8XWSUrfBSjU6/PveWX/K2OjnAvGzfRdx4hIecQrQ2xGaDIr079y6K2iwoi+rI8iVx/sG4jZ8he4+IAKXY7zO8Nir637ghsnoZrnpPGy8ZWyiDixmpxfKRUZM2PO/wViUiQoUX1UCXo4oZV4OuYj420SaQop63F79qkes4t+z+2GyaRyEY1oHB2zSQS/3x0bVEjiKS/WYoQFDP3ceZKQxOMPMs0e41MVSMGeaPcg9NWZS6nx+BEjA0UkrU83rDfZCJooLF7q/1/k8D9EQWQYTsE9n9OJmHfdw8iho37CES8Gv1OerzlHXCC9rWIRZRBjJ25/S5Y2w8Sb28gRwSroIuVkD9y+J57xZgOIKaP9RP2LOXvgdtr0GRhpCO2/xx+PFx8DKY2tdKOFoG1ZeTzmFn8XlHCWdNpquK+ZZ9e5z3HB+0kXKvgGsDVig9u+hAay3EgKWP7QBsoTUXoRWAV7I8iblLbdigG0Ui37ujjqGue3EOS8i4WQLPH/77DJIfA/zTJ6i9DRaap/4dY7uL5xNtiNgSIV1rYPBU3TscJrDhz76pizPpGJ1WD2Akq/UaF4RHpw0ijbxiXt2mLYMraTAsUtjKJjkEIa9uh7FQdls9qHK2f/fmXpttnj6GvHsWvGsXMc4LP01sgt2fCD1G6PjUZJ6lQYBOlPCx0Fbt1pPJpHtozSCTI3KYx3N51YpQx7q3dsjgm4NGMU5cIQSRZ5gUKHczeP5IHa9DUghLuoOx13aT/DzSfjZv/DlUgM330UioYV8WUI8Z9aRe+XqjEYEU3ZCi8DQzRHZbTmmgVvp3DsbK52cwzGDzh4d8iggwJdSbOYRqQ5F/HsbVCQhNtKQnirph0mj6OW3hgR41YYieI4LSZ59c+kyAaKPDDJmwydH4ka5tUx+om76hJUK9zCEB5ZhA94d4BiP6I5MhIZZ3H8soXCdyP57yQkJMUkK9nawWs7+u87mFo0aGRLlBj3xiE6t/7LTzseGkCm0joHJtHgoTthXIKvg8x4dtpDz5vjQTX+wU1rEVg4e1EequFZLUQK23SvdNF40ciBvS6u6dnoPsX6e+Umuqyt1UKBSTMmjajUrgjCoYKGbIHNbIQ8RgR8B5ju9GoWPqHogfQt07jugBHUIZIwigs93MEKUp9QZ/+5yALXDfFEJFHuPE7+/rMJHeQzpXo4R32n9nVVpSfM49svONY61KYkEfdmfahWxV3JcRN2wx8KLkmY1wr21moW4+5sFyAtowoOYEhMc7kpvE7w2gRU9RZ2C9Uy8xPxHyQ1Npm9ZzllxUXZpKU5KDRbc8JaQpIbr+Wh7C8XvXSQOU6bXeZ0g+xRina1NI8CYZBKnGUECodxmrqdtPT83IGgi4a6Ox6D2xJi0iHZmdG5jPx6FUkCzbucIMOUxY7xUiW+9SyOfUCZDsG5KSh0SAesygaesUaGPF1ksNmkWBBjULE2ROaCmFwrs212wRNciccf3ihOurrlvZKdA2HVYECsK1RhF/PU1Y71RaCaRRMAAkkmHDlZjjvLYwLHwdtBWN7NKIEP3qdg5UPr/giJmz6C3rJSmNZL3XEgheG6a3KgCgrM0JLWuHgvRUIKWpuIBFusLmwDYoQ/NSLELkrjhhR5sTrl+csd5qtJsWgatkAAh1HbIAnVdja1qhuu8BbdbOyN5AkqzqIF0Baro/pQWgogXW80PD/GYFl53C/nuOD9tIORRSFcNTK99iQ40JERInDr9BJzxpnY4IgZ0+3ej59xjM4BsuSo3uqYGPD567KhPDYMPRix/dlSE6skU5RmYBhWw+g7ivUH1Hj172542PNYcALOHp52TabP7j97iMj4r3CpyGPtLOaeSWWkKVl2LPpAbjLIwp92hocwR0IJEYl0TAcVLocRyrASIBBHjhcQHoBipeGMZn3Cf3uar934G+kQjQmnn1pgvnJAmPWtyr9qr+6AYdc898WrOlIXEzJurtGbSJUCsHKUGH2lhWrN8tpNdVZ4VZACAtFBu1Oy9BftnML4OnynsG0Y+hq25HDqRDldkM7cGcG9c2TBIOVhAEnhWZvTxfownfJnIf5ytBKa36XOuk3bLVqCj5xiOTTRDhed6147YOte2VwaNfkm/cAqu16+QBZbhxFHWHofBzjIBKke1+EBfQXzMdX83NztD7J11HAs9dGoRq9jBQnP9Pe+8ac9t1nQU/Y8651t77PdfYie2YxGmg4UuhIYSYBLd8oiiWSFoReoGqkQHTolaFBBIqQQolXIRCIhUhtQWlAonyg5RApaa0UQGZ2LREcp3YiUtD2jRRoiQkOfGXHJ/Le9l7rzXn+H6My5xrv/vYJzcfv/ae0vE5ft99WXOuueYY4xnPeAahoi8RExIRIh1zjtmYqz05CtCg6JPnwPe2kcCIKowXtYjamHzm3GW5PicTZf2ZCttOoN8nGCfbSBXl+lsL72aDCZ05ePK49BJ5pKOMeCAPWBgK4pEcHulImxGWehOq17jlIW4PswZSs2RqWBeXOGEiYG9eH/It85C/GSB5ADiKYns8HEVpomH+hFy8CBElC4upiAfM1rHWmW1bLp1tYwX3utoHxWAWBCAngUPjMlfmYhOJFKW6B1jUApjahbjPxXNy3EWsb55PIjRA692ORlhtDY8FJkkjCefm2gIpE08O7jBWz9aS8TzrwXszbfsg3p9HmhvwnnjqHXiuxI1FQtYc5zhXT5zhD6V42/KwxaVI7gRvqw2HhMI6K9Vao5QQgPWAeKiGugjN2L6HOjV+KnuDmdTn0OmAuJYW7+bhCilIPFv/2VrEO+0wtv5HNsJAKPNe+k8FZXlGldDSvcuRHBVwcdLWcQImf5dOepPlTplcM92fWicWB4XH2A4qxvE85vQHfpBqnRhi8ehLINCoCvUF8WA9pcZvfLYxLu2a2nYbRWu6OJH/XCJTzXPHJpdnt1f3dTDJIq0z4lKEEckNnEdSDpJT7Rsl50F7gdBat9qxIWjK2oyGq51zD2v7Yxqirl8YzH9RMgnLuoZ1dmUYMiJF+/WxOHwJaBSn68+RhHwCOw9oWo8Ygpw1KU6IJq3T2OolGkRqz5B3Sb+OcaKNFJUijKVW4FUHa0fcEqvsPhiYXSLX3AqrEZ16vGE5VJkiq9XY/MJteSYzZiavpJs8HA4Ihys5EPqEMhcjRabNtm3YoQ6dy2pEuroCh4AyT6C5XpfS2l092WpdmGtb9QbKE8NS/x+AG29u2HS2fmEQYcm8ED00+ZkYXTEc+oBTs7FtObhGbc5SVChnXHQ4vLXDuNdGOEC/z+gvi+p4WBepG9E5hA3oCVbvBqhHXKpqSBC6NS96DM+ZI/dB2hJY3sMiuXbEAF50GE914EhYn02S+I2iISiFoFqwqAdHOhIjnpbSyiOsDfLRg2adQYfa+M/q8LTAWFpXBIRlEjh30WHci8gzRgGhqGc69sD6rPw77QN7kO8NQ0G3PzqsbUorJRHSTHOjhIlUl7R+CJ6XtLV1J8DuWYooWs6BQdX+LRcSzHMmX7dxL2J1RmCePBcdQ8rSJC8dcS1qbqD4iVHR+7ktoS96guqxa1mJrEvw3BGNBXE9Hn+vjrYUIK4Y1lXXtSWp5lAAIB5lKXwnUUS3A9ucNoH7skhXcT2wCQFQ2I+YvCygxIjhtBzi3dUR6aBM839EyKeCszuNfONUbxanw0RvJXdeQCpL5fqgqkpj12q6lRhL7XKQaUogA4AAhHXvebjc1ULjckgSqXE1LtTkroVZK5CxSXLxBkIy6YY8KKlICUjWRfp6xok2Ur65Fb4D4LmOmlvR5HTUCMIsuHl3JqFjfZfsRgYIYaL9HhubENGWayI7qAuk6jvoBqZy7fduDruuyAijyC3VRnL6GoPK2mvbvNZr/g5+qPlvGAJJGCNpkg9TT5i3eMTHrr3xumwoxTz3DRxSWGVSyNNJxrSy62lRRbvuCt80kEogYYIFLcTt5E802HTLurOJ+uq1CWxBtT5KD3nogSFwhT2A7FBcada3hVcn8y9FlSa0Lq+wUN8zO+7vBC5ClY9RphwU2vM6FYaTgMxbNcWPKkNjMBCk8LOLU2du0xkzkkM57qRNSDxOrMEEriS9J34g2Vfpvqlr8WQbCFPkQb/bqPpQaMsJOYCz7WpNDmtkwk508jY0ti6tSoM7fGa4mwjenjlu6N6bo0BymoUFiic7h1D3dGujY11Xn0Ou86j1nFYkb2eLOqktQcKuM2+w7+z3BfUDvebR5qZfpzC2sXK9YayvT3PtLobdEH42n1Pe+H/7se2dZ0MkxarQbAd3WXQYTotHHEbV+BrF++ivVK+odNFbP8uIrpnleRoSA+bJ0RY66yIYtWmdX08KFXIkqlFNlqjNKLPY9CYtFO/o+M+HUeC1EEBrqRdpIzGHZdphOoOmkZaLwwEAhAQyAFSGCu1sHhobGyiuLFqRf3cH8p2mIzfRXMsEinpAJ5GhMpmaxcWMfr/K1BCzS8NUGR+RvRnPzDDuRf/+MBaUFLA+lzDOpRnc/CJAB0UgD+0VVhZJEvlzQuk6gPY0Vwl/2No5cyCps8pyXd1BQekJy3MRvCee93BGDoq0BGaPS++mOLAqwhekNQBWyPJQIia0e6G9pwaXaIFsdzCCipIl1gG5l8OiHMqB0e0zZhcHxGUWlRXtXyZ9sIQgQiyF5LJv2aE/owYDkIhtIaiDN/3rGDzrYR2qJW8mUZQZAVKVCgBeeM1B1PAXF3M18OoIitfMfuBazsSssAuoGrKwavqx6QFWzuxhPD+TiHZhatrkDg0VxrhYINw6lyigtzocRneodWrNwWr1ekYsmDgmvUYpBsnpdZjzY5Cyy0JZA0pjwSpxYtuhGwauZJheWZlBoEAOhHEePeqNK0Y6NMvRfIgZO00FhKG4E2T1V7F5tuWLxQFrW+SY0S99ktqsWcK4CJJLbIwMB/LW9S1MJzJLou1Yzi6wvmmOkgjr06HKLmkUKKxVIRgBAGneK84AKgF51mEcNs6ta4yTbaTmHdiKNgHkRcLyuR1yR5hdyZh/WfJMdDSAliu5QedPYTjbHwtNpaBN/u3h/ypPqOa1xbfixOphezFrqUV7TAR0sZIHbPOXMsmdAboB21YIo6kCqEHTxHawB6P14hSqaRmCzBHok4ibaq6KgKp0XAAaBsXWNbKz37Xso7EgrIWNF1aqTcYkzDWFaeJyrJ06tXtvoAIetPaqiyh7vRAZCmNx4cg9VidLNJGpSVwhRfC5OYbT0dc1rBg8IyzPB6yeQ+j2GekwIh5oUaP2jcqLhNXZgHGPBEbr+hr1GDXb84dyn63FSryyBjEj73XI/UKEZXtgOMMY9xjlSsDscWFChZXAkyZiawWyNOYq50RUC4vtoFDqrqnTxytrxCtr8CwirnvvWGxRbLef0X/5ULQHWy/+1Byl68QIseSqxOhrQz8ihFMd8kwgmfUZOeyjGrB0qEuvER0UIgpGXDFlfus0EGrhJpM4DosLytBqilrzIk3yjvKscSUvseYJR1Gkd0jU9BRDxHh+hqsvnIliQbMWJVWkxCLNMAD9FUY6YqQVY/7/rRGPBnmulE3p0X/jxXOskkcAEIaIsE41P2fPtELd9nyUXrQmyyzWaEIjM0NnzDiHkZVExVI6AHEcRHdPjGvpCGEEuqsjuivSiaHMo+4DqhT+AZUCbvqHCgtOHGkjxRDBfGnLocFaB+0l5D5IucAc8kwopM0BzhS1fCjUaSmzDtRFrM/PcHBrh6yK8xZN1ZbzrEzU6VkX1gSmgLRg5PWWhPmWcaKNlN8M9Wik3TsqfORGRx+KQEJZNZhr8jnqZTEAqOewLWEMqPKBMuzUWbNNOWXzCVXWcXczaMegAqqGIhs5wb5sO6zQYuLHhhnVKMncTZagwwRGajCFAYO1DLL0Vgg8MYKUWYqf7YHUyniDxKReBKIgqcwy1qSvtCAoE9qx9ZpyKRj3qu1wqvehrSVxx0Lvm0FXbbM6yQ3IZwQArFAQIB6iavBUr1SvLXRq4PWD2u68buSs3MHgvbFS/b3Qtb2Htr4NdGXrLFGzePzmkdseNUNITcS8+RoAnsewhDqCOgOZ1evXdSuqs9gHucwcBSSwkgFmWAGtLBrB+gU5RGwHv+oLWi6SY3QNxnZUuSz9u4WDLSfWPmNmhFwlX9+r7EFhP8KjFJk/KvNuyNp8b6MubnNQs5ZRPt/hwElNU3MP2zSCNjR1aJihjuTG+2DnCSoJSv9te8uhXAYKa9dt2dhaC4mKBrRgjD2vzFK82+pD+po2znOsRrotsnUCha6z/7tZK1HpN/UdNKmV4+vaPoPT9W6+9zrGyTZSzMinehzdMkOe1XwCIB7K+rx40D0B6WjlB2wcCkqRB8lbzGuYGvK03scePK/9gUQ0OW1sBsA3HQqDUhAarfaNIvMWAfekpeleqAnrSH5QcLsJA1z8EyTN++hgXT9nbwYuEDFVpTsbu7EQpO7LusDaNYSAcmZePUtttxFWqrCuD7pPsYEWwIPWksE9rLZZ5GZvmjAWsEE71oPHIg6genwkDDQosyq33rg+IMSSlC9XBHoDCeOLhoKon58ORyy+EkXSSB+gElGFbzMjLYU5Cebaw6kdo4l5iqfeXSXEFaHb1y7NS2NZKYvPIuhAQN+jzDpJTFudVBMxcooop2YYT3cwlXxTCg9rJdYo+9JgUnM6pFlj9PV2qSlT7QCJQrw5JaSF6ENBXAenhx/dHMEhIq6AxcURcSkEkHi4Bkaa1NWwdhkG4GQjsW4a/QQAZqxIiUiDHNRB26O4EncM4KDvVyfGo82mbCAuM+aXCrKKFouSDFA6dlicNAkf1tKSvN/PUpMVA4o1+MvlmNx20PvEhRA6Y4bKz6x/23A6YtiTCGfxGFzaalq+gIoAWPonKGmF4HVsVmBuebs2j1PM2WLtCaZQonUGD43TYaKunhczQ5+5XgcpIYEVPRk3kB0S5ykMxZ/5ktQptpyX+KXVKNozaAiS3tf5pYySBNkYT1VDbc5MXOJYeUMYBL5Oq4YV+yTjZBspAMPphKsviBhOy6HV7TNoBMaZan5lYbDEx6N7uTQUhALkZDRcduHLOBSkqyuBVhq2E5uOXalRwzGJJG48FYhnQ6QbqaUiA/AW36r+XWZJvL7CoCxFlrX+gaSz51w2WViN0oqiFPDpPRRVS48sdFjv4NoFCZJSmLKKWGC4fKpDiY2xJqC7QmKkrGC5zXdZcnewaCFMVAdM35C4esXELAfYSHCRUysbsLqzhpFYFmKMOUkTOk+4kyT+ATESRlwAgDyLYkw1mon7KyyKHFbDmQ6r87FqtY1clekvH9brMNgzRb0PBZb4DSOjvyKHSzpgdAcCD4aVqEkY7CJfEFD2eqzPz4BA6C4FxLV1YNYcQZeQ9xKG0wItJSIEEnFba/nAXQROdSgcvGSAUgTPOuQ96fZrfdQAYVhagj6HIIxXhqheDGJkykr27rAgLG8mDGcY6YAASuj3C+KRGhhlcdQeTOYSC3MMY4Yp+udFp3lREpjQomu9vni4Bq1HcJ8wnplJjU+uxsgRBCsh0J+HwwGzi6Hp8Cqe+wh70Cq0FNeM/mpGdzC6McwxCftvOR4rTpbbJK+jPjjE5YXikbA8J5ByXAP91YD+YivYCkdjHOKznOwsaKE2kEhzrPr59owVfd7aGiNizVM1kHyA5MK8a7jeA/+7RVLcAbaLokrcCpA8uhYcg2XflGy96iC6w6FGU6627iQUNaqqdhLGgtlXCjgRhr3ZJOq1648rRnd1Y91HKTaPAz9LZJGoesqt9AlgG8f+NDfURSGL6IpF9R6Mw68MnmtCaYCH3gxUY7XltVYQ2xZP2nVvmwsMMWvrvRxi2c6GYYf1tBq/gSNaRhK27Aej4G7WV26rJzpWiNzCH8oCMkhm8nkGpYUwuQYOBPLWogr3qafddjCdavjpX4WF5GDQncFbxr6yxHxhUFH6MhHY2jJsw3/8ntcIUcg3Ck0wUJhdKseHzSkQEGKNYq26P8haMoC20aDBLTBqpc3NPOPElS3Wrnuz5qC6R2sbCaVY268LVQjangmLzBrYZavO4jbI2aA5q69Sj97hZ1Vqlz/2M/bn5FgrC5uP5lKMQeg1SRaRJAYzgTrA4doNb78tpmUCAgWUEo+VfHA0QhFgSiG+Pvos+9nhRdRwVqYPLb3gevuOD3uWC0uNH+z8IKAxAH5tNP1/ALIfNCc4WbPJpK4jKjFS2OZQZ0w+F1YWqMXTQvyQjw9SEwYGskZ2MLIYAZuXwMfnUiFLHH/9NcaJNlJ5IZBKXDP4iKSviy5Cv18wuzggrDPilZUziGi5kogjSkSS1LuwZKpDWgo9OEbP6klZR9siEZvj3wW1qts2Q1D8dt55JOMaXBaVjOrBF7nxYZ0RD9bAMEqR6aKTolsbJJER9/KZVpTJIGDeIWjCVOolsjaey8fhLBcmJWVjWW4ByKdncG0wrZeZyOo0Bsok+qVAVKR+YplaAqnpyPV9GoGVWec1KyZqK0nl+l7TAHQvl8TbE6iN0R2MDpfRMHrfqgCNPsZeae8AFQLlqCSKHskgyMNVba5o8MqQ0T++Bo1CTFif1n5OoyaEFxGJFdkcGDzvkE8JQUS6vCrdmQjcd/5voXjH6nDYg+p/19q3ljpdnYHmHjTvCcsBYTkAIWB9rsd4NgGsKKod0uoAhYGQDuRQjyv5uReKD9IiRFRUqkfuebM2NzkWl90x1ioB4OZ5sW7WbFDmkGskbtFaahNLVCP2sYAKoSuMuFQR3piQlehQHVPCuFehK1Gu19dsOZP7qwWziyvQmJEsV0fCVozagPDUyJhfEiX3/nIj32PGTWG60pMUDA8izcajwsDKfvSapwLPwZYctBaRANg5U++nqDYwTPDatDkZ5M6cKF3o6yy3bM+kEnXk+S7udHK0yClUQz5KxCNvkDwfFflqsDxDWaOntCzor8IbMELPv7hidPsaSZVqhOS9U0vEQRVRMrY63dvGiTZSZS6spbDWdijNjU6HBf3nL4GOVkDOAoNBPZ4stSVe9xECeNFXfT+g3mg9YJ1yCsDEVrmgNj1UDTF0ckO5s8+HfO5ClbatyNUnITtC6O8CSdLh0jvI8ixWuEUHaz5LineDX1fpNV+hRYdhKa0RaDiec/GeUYEQVNkCY0GZJ+RTAifFo1EMOjfaX+1QsoodzKZkXpR0MnmIbFiSetZh/ZyZNxU0qR+OlcocV4y0qu+1g9TEbMO6CGw36a6sRpVZ64f2YN1AcyHRJk1QSLUHjdIZFnathSV6GTPSpSXiwSB6jGEG8RrlcOI5aUdXnU+fsD7XgxN5t1fPdczitHwkhq2HZ31BjUD88LIDPcAP+BqpQOSo1oMc+Od6DI1EUqQaldIIhChMOIAcMjXlbxqywLwGw1nU3IoQ29DOugCmB2V74HYGn6pDYLnNVvhZi+cBVGMOeD4prFj2YSfFsYDVONVLGWfCGiuJMOyRkiqkwHiTXMEXgPmXtSzEnlES1mpYiaxXulIqTGiMuRZBgH5+R7C7S0Wib14xEGpphqUTKhMvIKhDZ7V5k5qyyT5o9gPIdfqC3xfNPTVGynOJoyrW67VyoiY3KHspZHFaxMCjEpw0oisBDckjSJE2E3gk0cwMhLhmpENuiBgW0dZntpkUitrZ66zlPdlGCpB8QVqxwx212VyjN6bDHjzXdTOv1AvtFMNt6dH23hDAFKonW1iLXv3D/e+qX1UjDoF6eOraGXVdvWZW2MrbtjeQWwsxlj5gPC/trPM8oszkuqwKn4rMKawzTBeQM9UDIdT+QDTyROKlPSRcyLbFvRt4zz1em8coD2doCw1RX9fciDpvgjaqa36u3thmYzkX5DQZm7F67JNv0GhIMHZTPiA/CGrxr15bC6PZPFn3QIYcKmtG7LgyyPQaOci9MsUAg2tDLhVaLhuHrzO56pxsnYQyrQ5BMBhMDQhN11veIx/mUK8ZI/NoDQJt1k0iSrgkTmgEVI+NNnraBhU1kZWvPbPXHG4r2nWtSyP+GNynzo29pm20KQ6eSmENFkHpZRVdIyUk+GZoL5dQ4ejQ3Oeie8r2dvucGtnIGMS2Bs3+DaMhEezkK4FZ9SuM5RnkdxOFfbsnhWrEZUbJnrFja9f8HQIIZXKmsMHNRHKZDcMW+rxtTxugYek12o/NOWb7dRO2DSMjrqGMW4Aje1R7vCK8ea63bLdt40QbKRoK0v6IeFQwoQZrwpYDgRQWI2PYnFqg7PVTr0hzGGEtjDnSKKZ9wHjeg+dJGs5tCtkqHMd9ktAYuoFVZokDgedJHjIeBB5grrpuUSjANEjCupzbmxyclJXps5ZE5cHzOxzcNkPRGgiTdklHwnyjEZhdlQR/GBj940HOgEWH1fP2MO4FKci9MiKtBq+9oMyiSFBMPFRbYjAQDxVOAlD2elG01jlaDYlDgkrltutvYSrvfbQa0F0sQCSsz88wLjpwgOaB5AFxSR3GVvVpABVSChAK+Ia3H48GLL6cvEmjvEnuj7eASEH3ib7XNNmIQBwRCtDHgO4wVJaVvXSRUPY6MAFJGWC0Li5NI5R7rXtT4gSXgLiMlQma7aGOGM/1KiBaDUcgLeDOkKjJm/RFrVkihFWUZoXQpPZluSfpSCBfzkLOgKpSp6VIZYXMKrfEognIXJ0ji3ZiUMu6ceJsogJ2j9cjgjXVS/V93EWI2rPW8gGTPCUFrhFBClJ0H8S5DErRS/sZC1RjZh77OK9K8pLYhzp9co+FIYgNw2XRpezD0geMC1NSP260WxgWzOgvr4FL+jujjneh5nojqUCyOM0h1jyxwcFxJQ8vjUA6zAiHg+zJLkpeOU+fI8+FB9IIPdamqszgeRJBg1TJXdLwMQuJhAgICWMjC2URT57JGsW1sPBoBGKGt6FJyyK1eJojM5JHOsxYqCHO1hstAnkmf/yZY/nctJK9xs8G4gQVRjwc0RkbbbMGiYTuCw7wKuhFJ+0fgLphxoK0row5rAewsdrUJbLIhpVaHlZyqIv3Jg9T0V4vABDGpsh1JkWuVLgW9TLL4TUMQEqyIUtS9lbvMv1SRMtO6y0UsLwpYP8lA2iepb1EISAT4uWEbl/gz9LJoRdXjHgYQauAvOhw9NyI1dmA2WVCd1mhMquPAeoBrWwjE5h1mrQanNJLC4Z4WECH0g14coQ10aofVMw1naW5IACI8wSgc3ZRXLI/YNDD2gpUaSygozVotRbI8vRCaLGARxLt/ghHA/pLtbFi6S2vpUYqQQ5l86CtgJoIRJLfolyQrmherkDWqjDK3gzj+RlyL0Y/Hg4V1lpqDnTMlcUYIzhFEEeh91v7bHd2pEXDsBdUOUGUQVAIIQU9cLRouDDQL6RQNxIS4AcaDQXdgcBWTpH3yERClrQiEMv3pKPi2ox+75id7mzdkDeNFA0bz5sNK9KNAUDy91o7iclgMapg1Ue0H6vRACTSM7FTMbq2icRY5VnAOIuSd7QI1dhqCrmCa9sOoDFyFnUTIc+DKHLodTnTzSN4eGsYWhfEq5rLNIUXIgQkcFfATMiaszL4C5Acm7H2mKFKGMGdr7AevT5TjDomTp5dz0RKKQVYI8rSRYyn4qTGi1QTk5aj3H7rsmzRU9QoSAtzXWkjV+YkqfMYVvkYUSwsM/ql3Lw8T641uEZonFNZ0zgI9TyuVUj6OsaJNlKCfTUJ3faBcciG678RjkEv2z+3NXQVV/Co2rzfUrRENLshdO/LKLWAwzbMDdQB6GGQtMW5GkFSKjKa8H8yJ/13UM+TlbtFACcWz4WskZ48WCKdEzUq041iMEAIIKoV9ibv0rL+jl2DrsU1l9CMxcbDNcH1WyizNNFTI7FUabDNvO0aikGkBdKaQT33DQgSRYkjqdkf6m0DcOUMy5v49ZmDoyoI1nSRLKsc6rU5zJqrl2nFvG0xKTet7IlRhYEbaMXhFd0rrjZwjXGs84VNs9R18sLeMmXMMTXPx7WGQmB2CFsOzOGxjTxNfU/dA/XaJPkv6178ev35ZLtW1GdM18fuDReuRBAiabmTpZ7JekYZ5N82QATY1SqCFvxOzggUhDGgtOxNQ2eMBZgVyjaB53beBZLLbOZAk/fqdWk0JueRQZv6XU64ks9pm6v62mluyvcM6Xcqu5RT7fLs08giUhsXQlTKewl5ERqyjHyGdeIOgxpoNcwe+W1Cu3Y+6POKIHkqziTLsa6tWqrE1MZ+v45xoo1UGDNokhWlWki6HqommC+qKqCP7Ks2ocq2+DsLDEcpwXTWHEMeihAyhlHlbdSLGntQFuhHIrfgtGqhXAM8iyiaK0GKEk2liLLovLVEPFgrDTlUWRfAcx0u488kHuZI4inOCrgj5IFAgyQzyhLo9hNoEKhwdiUjLYN4MlEaNdameMpY6kJ9QA0rt8aC9tCT0FMno42eusabM4cpViMSKIPGqA9HRn95dFac5Qho0EaTdk+CNjsE5F5lZeY10liISn7R04ZWA+JKJHJCjFpADTfQAGStozQSRFSldGXhWRK69HIfaCyilK3efzwYEDT/Z7JIXnCrw1cptDlNqUuynwNijOKKXX3AZHOoGCVf97AdZKgequeBjMvhB2zRyI8QKINzAY9BmIlLg0wbY9fm5/S6yqLDcHYmzKxVUXKBHZ663k2fIaRYJcQaVihZIn+0vmOlronmpEjzwoYgQF9vBI0wBp+7Q2BDQR8JUZXew7qllcvqj3sR4awo0nSHjUNRclVMMVgykjekpMJIR2Mt/B6rcbMoc5LLtL+ZVGNPfhaXGfFgcKYse24c2rMNgrbMkxooBo3T76QR7lR6E1ZzqJQpO5xJWJ4PngoAACqE3HcYVZF9dTZIoXJmpCOgvyqkh7gSn5lGlf1SFm1cNa1BzFGmZq943SMQ9HzkQIiriK6r+8no9fZZW3OgW8aJNlICU5ftGwYAxtFZfSiSEyDrUttU5G8qdnPrITXetP4S1pmXhwEUghy29r2awOZIVX5EYQnBxoPSoxkUhRLNkaSYNxFCEToxrQfJg6X63VQY3DCn3BuxNhyJwamA1wF5T4wVQMhz8iK8eNQcjiQkjKLQiidU7S9VZ9jaB6gdjVfthllprqKXptGKETHMI1YZKOubZY3bvLOo0udtXTmQBLZ2r3IBwYpLVcGjlZaypoODQm8pCWxLJE5Hau+bQlJWvE3w4kcrjOYk3ZuDtbFglmLeAmFQrlX4lUiU7+3w9agSTibwXj8BahytOFf6MDEZPNWyUKtxOjbM41VHi8350p8DLFFgtP5JFTISA4zjobF+b+nixPOmHPTaAxDYv9O3Q6SJkZ7sE6thW2nbkhRBXYLRrZ1MBEyKcD1qaTeiXnMoki+kElUk1khTNWILY4+SJDcZVly7HwBAIV8bd9j6IM6kQc0Hq6kT2+wZ0PH7YhBh1Eg2LkeEparEzJKudWlYisq+MxWRYZjS/vUZ8/5VYxFIuRTpn7aQUpBxETCcog0jJU5ISXKf1+cI4x4Q1oR0KLCyaCDq3M2HV8MYrB3O5u2keju8dMcMFonjNinHsfPFHaxngZGiUuohYElpPQRp2Ng1tkDaTI2ah3JTmZxI6KxkxZdmoEaF4bS2iRqVYR+68IJn68aKwdtV26HvHlJmECv1l8OUkVj0oVb32KvbM4t0jV1Xah5gc+ZImDYhSp+YPGuuUzehC2FahKMPjElFxRUUTm3mp3CLY/Nbqvkn1Fg7KAGZhw2n96phayIFvw/GsoRGNYGqxE2r3tHAfP7eUj8fMcq1tIewef9qsGC1aE2E4uQMkybK5P2rau1RMw+j9Svbz6GwJpI3g8OxSg1Zvyp0FaKyZLZ0jNXolhkxS4ROpeg1shgKa9GgXrwfAAE1ctw0QhVQ8NdK48NY71uAS1RxBMosoOQI6gIoFWnIWQpoRcchwM31br7HI7VQC7mFfaZrnkItAylF1gdQQ6LRVNNmxWoDa20ZnIlm+8F6GXmEdmw9DAYkUcyg6qhtvu7YnBQCdhmwzQ7fWz9DWH0megyoY4RKHd8cbOtGimroHnLIcPvbPL/EAegOZE+FgZGWUm/Io5xZHOu1uKqLnWmtkQ5UX3d9tqY5157A2doyTraRGiTX4Oy6TmWMAGl0Z4dTC/cdrRCHsT5INlo2X0qiRGHdJ62Tqib6rUFcG135Nakga/X04WKX/lo1VrDi0xikeZtj/sX/7YnUWOGXtATS1Yg8EvhURndarmtcduBV8MOldCLAvD5bC/xsY8UBSEekh5yE+UzaBlyLJXsUpEPxpGD9tgDEg7V3Hab1OF1HQGGbDAzTH296Tg6rad8nh6gcei2VfGL1Nk7XVy84dFVBHqiwU6uw3jdupTsRcr85Ch0/7ymZxuSvsqw9dK/Etmao0WE0Y8jzHuXswou5fc6mWk1C2im96EAWSgjBYJyiEX7QaFvyicMpq6NhhDEiLuVgTetRTiWT/QEq45F130Y7TAK4P35yCaOQxRnj2mOpzIXl6AdSIAynE9anBSqT/RH8cLUcYro6IB5sfH7j3RzLTWptFPedQM7WRVkZXzyLwu7TuYXlUNdQoeQ8C86EjKuCOBSP1I4x84aCpBFDXKliuD2TlkMaC0IRckEYiytTTJiq3DiRRq4iAiWoZ5gx6e9mo3HK/JqKdBEIWSFPRTZotLSEY5bNummdFHN1xJPVKh5HQwAxIumIMbuUa25PjXU6zLVerL1PoTrF8hzJZTjk15BcvHGmXWP7GU0ZAel1uWj0dY4TbaQwZiAyQJJHMO04BMgGbPF1f88o8I8OK/o04oK/vonO4LDU6AcfNxDcpi6eXZvr3jFDpbjrsM8btf27CsP6x1jt1pjV0EaZa9DDYS2heT4NzGZyUI3rJF5gUYgxMbio0fEQXv4I3ZsQWD66KjQLy4cJ4KNaMDppsGaafm2UsDE268wAaBJf1ypF8dhDfbhYk9oTUoNCeQCmuZNi4WA13pN7YIes4v9mYNqEeU04B480k8FNNs/lekoCaOe0MT+DbK1eRpLjmKyTNHeUfICzBS3CZrkPJcL7HOVevGDp7RNQOs11ktwT6wRNjRMBts/CJDp1j9fv6cZ0iPT6ySW/APh1cBLtPM8tdA1EuQrgZZzmd9v9gC2Gyg7dXnJYgcgPxZLqPXGtP33ujD027kVtcyEQ30Sloy3E1b0kCvM1Qq6Had3bIvQLuS9KtqHGadx239ufOTHiOoc4FvLxZRaF1euJxS1fQ/DD32rnvKbMc3WbbxLCQlLh7LDONX/pxLPqRJoBN+e4KB0eYEV+rv3c14k1Z2/jGDr681WME22kTLBUjAxPw/ttHr4NiyqUSswB8Ap7oB5w/kXNIQUNye3gRKlJ2o3XV6/mOKxgn0Pu+Ybjr2s8Evlb4ZGGNkqpoE9ipI4iIwPgwODEKKyNxlaM/qo9RPpRWhcTRq4sKPmN/39clYa8UIujpTdU8ShlkyAx0WzbMu9j87P/VU/NG8wBTkqpMJm2X4hB7mMpch9bYxWrgoFAb1pvM2QwRq0DC/7ZgHjXYK3BWo+wlg8yL35yeML2BinjLOhhHzagG66vF+bf9EBiVfJnPSjDKO8piYCZEAd4Fitk5u8LEGE7VO3AdqiDsAldUQrIs4g8swLfMGFXAnB1AmuT0nrVcW1MV/j8Jw7ilsEjBNYLJASAdQa3jDvoXmjXSnOdea4Ua4WnzShVjU5dl4ZpZ6QSh/kAWMsJANWQjQXERQtgi1SfFExa7fi9biJpDqgCxTa2FbDT9Dn2eY6Sm6JAmk6oDo10W6bKCDRWZMsuDPK8FlUyMd1BKUMxJxSuSN8WmDuqA9Q52vOjkLVDoO3euQaU69B1EDY1qzPlKQ4uKKRaltdpzL9qI/Wbv/mb+Omf/mk88sgj+OIXv4j3vve9+N7v/V4AwDAM+If/8B/i13/91/GpT30K586dw9133413vvOduP322/0zLl68iL/1t/4Wfu3Xfg0hBPzAD/wAfuZnfganT5/+6i5mtQZxEujHlcnNk8pCnGgXkEhqkswY5awssKiK5J16U3oyqOHiIDRiLqHeRGWJ0UohofolelB5XDvdsBNcN4D7UOssrCeTex418jA5k5KCKw5zx+hmI27aO8JYAg6OZsghAQTkvYJS5Br6fcbpL6yma1fsodRr0Wsqs4S0J4d3OhqlLUhmmacadRco7RLymRmG050wha6uXaLIeyttDPek3DPWw0YhJxqKS9OAedKWonQCc6BLQKcQnrX8sPunYrXcRSdwWFFoPBwR9pfgwuBFL9qPAMJ6RLp8JGuyWteIuc0Ptpz7a2iOWV6gRGsWKGGqQVg2TwCSJLfcx0qlnMjyh/LaMLLAKkVkf0jbptPYIQ7yuZbrLLNU68WaglPflgBoqfVbtv4AME9Yn41Yn5YDMa30wM9igMDaUSCqsbL6MmgEdZSF0j3W/HCe12vx72euMkEE8CBsV2ezApOaQz88NRrkmcC9q/PCXiMGuoOaUxFqeJNDBCmMqg4HaUG0WiRBXNjJMQAQDgdQGesedwr8NGftkal1VWhlngxl2awN6FLt6Kt7GSx7z3LkVFjyfR7lh6kTnFVX0Viw3oU3OPQpkaJGZnvA+qwqdEQIIcskwJxk06Q57NwJ+lmdyrQNGbTRVmNSt9W8V4QT9BeJpai7/a5gxDFyBf8nG1+1kTo4OMDLX/5y/MiP/Ai+//u/f/K7w8NDfPjDH8bb3vY2vPzlL8fjjz+ON7/5zXj961+Phx9+2F93zz334Itf/CLuu+8+DMOAH/7hH8aP/diP4Rd/8Re/qmvhMQMhq8I2++EoB0yeGCgAAu0B1RgAChsVN0he37TtIGoPqVbl2j4Hzc1rI7NtUJH9XOEmTx433p/38WGunrPaPg4MjowYC2ZxRKSAELiG+lF+zyEgroF4Za3XjnotbeRj2oYNk8jzHKpf6A9iw44U5lcED4x4YJ9rdOiNTWjf20QdE6IEy6HouRWytWkKItWrJs8X5BrV+bprNX4gcBe8RxANBcFkhLoofaiYgbU6G6UIQ88+04ZFaM21P9EQ0oo6KrFMozAGrAjVjZb93mCcCHiNir5H2jkAZVASBeSjYTmcQF6oHNcFGGodjn2I0Jqzv15uICN3otggRbICRQbtAyRtwwFLynt7Cr0u63hc2XfqGPTtpGVfBciBhaKQvDH69JqI1SEBwXKTVlclzFDJmeaFHNLdIbwmCg6XNmtLBNPLrM5r85yq9JQ3aAyoTDqD+YDtuoWTG94c8u3Z4pPXOZimntPyuUJuBUAo061CVQrLP781AlYSAjPwds+EjMUBKL0a+rCR5/IzgI8/p0DNJ6kqiTFRJ9C6PRal0em0+RBp3ljXI7OfaSLRH44jLdcYX7WRet3rXofXve51W3937tw53HfffZOf/at/9a/wqle9Cp/97Gdxxx134Hd/93fx3/7bf8OHPvQh3HnnnQCAn/u5n8N3f/d341/8i38xibiuawRVBE9RcggqMtoOms/Ac+nvw22uKCVZXFUC4BgFhrCNwezkhgnk17RcQIzATJLu6JJjuRwaT7I55Cb9h4oagKAV46X5XsCNiH2GyRRJq+mAMBKOZnN8cXYGzIT1WpSvUQi0CiJBciSJ9/HcDFYHhUDSMv3KWsgBbc5E+1sJo01bt1ORebZG2iR4ViPSQRBP1tSt24e0hTLbfIUeAgxNmMdQN72yy7xgmmvLepGkGqcRafu5Wbw1GsR5KayHeYBHHZYMd8al5XPskOnsMKm5EB9FIzcz1M4AJd8TkqORa4yr7DT6oFCt5GLgeYV2TDqlsu4B2xYWnJPcAxP1dejtUCP6KMa5EPl7KQeRU1o3BhfQ4ldTAtBeXaZ3uGaF3SQfxoEdShKyglDpqRj5QwgttdV6ddomkZTmQ/webjgvtsdMYizaPRoZ/X5xGbDuoCCsihbn6l4hArHAZ22kIXOuTpgZ1UICk5HB2Ob8tNfS/MwafKIUQB0bmAEE6v3d5swUeP7LcoXcBRTr8d7UlMk5A4Ecc8NONaSGdYOo4YorWf/UE/KSUEax2OlIzoHZ5YpOtPff12fz2RxtLZWYtqkaQuTBotQINumVbWkL0+62HHQM4OtszftNz0ldvnwZRITz588DAB588EGcP3/eDRQA3H333Qgh4KGHHsL3fd/3Xf+HFwZiQNmbgWcR8SqAwyXYFZbFEylnTmF9yykAQHd5ifi40pD6DjzvZeHmnRyOIwFD8BDfjd6sB+/NUfse2UGWXKwVIfhN5E7+XRsXiteXrhJCAQi5QoW26a2gsI3ibGN2EXmRULqAtGIsHhOmFUqHi/mcbP5RDB0NhP5SQLcPhLVASEfP02rzXupv5pciTl9eieJ6O+a9JKatJoYBdCxK4Vrj44YHyvTTDUpDnnqdlkNayBpjLAKn5QI2eMaKqmPxw7fMpKAxHI3aaJJBR2uxYa2RaCJmyzswIDVLLPVKsaEsC81fvy9nhCXE81uPNcKwWqsQnElWeybpa68c6v2K/novXiYgrDLSlWWFPdXJKYossUGS7VlmTkJA7ZTaYPbe5iqYcVMFBdI2CgeD1PIAGJ+zh+GswNGS39I2FMuEuIqTaAGlIK0KyqHsq/5K1joj9rXnGDCzomaVlgIzuv0R8dB0LuEwcGSueo4mdto4FTQoXb6lM7slZqDI94x7mrcZGXGUuqHZlxn9JSNUFC+jaAt+QU0MGUmm2uRW7F6aoRBdR0JYx6mD5PckeBSR9zpwp5JgYe17ils4cFvUZZEZE7jT6yKRHat6fpIvskhQMDuBx2Adbs05LhXepCEj7a+9LpEpgQNw6gILWSIz0v4aJufl19ggOb5XC4sjPwiMC2vIGuq9lzMNfl+7K1QhepvupqG2c8wEClJTTP8k45tqpJbLJd761rfiDW94A86ePQsAuHDhAm655ZbpRaSEm266CRcuXNj6OavVCqtVzalcuXKl/lIxXu/jA4j7YeGkLoq1dE6HqhxQUAkQ9ncbLengUds/JGttgelrzLtC9aKkcE7bnztMQuDAQmX2D2eP+uyAFa+y+Xz9Pin6kxCeMiMdicc+HBLGowBrjidSN6Lfl47ktRyAPKMqIpkI+XAKtbSG0Ys8EyEk6ZdTGypuzH+TxbgV0tSOvQ37zKELyyVqZGItBWo9lHl2jQbeNvjFa4Saz7a1jCy5HS841EMoNNHXxLBGFzjlPog3y3KwmXCqQIxpElnbCGORfKkb0gIYfZ7tsxiYwHFoDifbMxuv2HTOg1SeWbNDrAePSrxFeWxqrrwoGNUAFkjB5ghnyVkL+yqinGtEtEgApE2MiOeWCduLAGkRYYcgs9xju+/AdRVxChGgWQ89yEPe6IsWUCGmNkqYRGWVpMClIVfZZTj8DK9BEwZjc50aLXMU+DgQI1jH6xbu0jXdPiluvovquUAEMCTfatuCyFVqDOL2+erFuvp/geszhrHUeqj9Ed3Fw5q/2sg9tXM/Fv1ofSYjONTKibx2zyBEMCFttOM4hg5sO8ts3tcxvmlGahgG/OAP/iCYGe9617u+rs96xzvegX/6T//p9l823l0wfTHSOJmlSC2sR5GAIVTFYADWwXYCRQFewyFN9EZwCx8W+Q/pR1j9jlN91cMAQzwRVJsjyWUSRfSBQEcbnkbYMGB2wBuNGoDRSeNKusbOvwykI5H0yTMg95JPSEfigQbT6htQJfWJ0R0U8WoWs2oEAJjmGLPi/W3xanMIOJ10QwIIkHnyaj1VKteo1g9ujQ5tflXLTr1de46SqmbHIImZUoQWPqrCfPvAae0U2/vMc+ulO6953pNDjKgWgtt+sNtgDyJBCh1hdVWdfE+ySEr2j9Us0boepNwlQBs6CplCcHpaF0SjTGe5HmK5P3Fp5AVMmKPEEIXqsWFkBQKDkecJUPX80sfKeFvJzZSyhRpJ2vyJRSHbmHJ5HlH6gHiUEYd1zVXq6x3KA6YMs81DR50AUr66q2/YuigZyd9nB5iy+DZzYJPROEpOULH81mQjkt/TSeuPplFpGDKSAisCXZbaqkcjYDFAreMohl+UxqPWPG44aO0eCzWCQBDnR3QEpzJg2FijScPLKGs6eQ4DPGfnDsCosKr9uE/CnByykEICCQLQMHIn97B1ZNQZC0N2gd/SkdTUUe37VpSgRBtVvZ57MwOv60+FQeuMMGZcz/imGCkzUJ/5zGdw//33exQFALfddhsee+yxyevHccTFixdx2223bf28v//3/z5+4id+wv//ypUreOELXwhADooyixgXEWGZJFIJBIysUFoGHa2Q9rVY83AFrIREQFF7K5nHZ0ZFC4IRQ02kA+KBB/HKzauXAuKmiE6H3IjjHiMHYT+FQKBUsWanRFt0wyyMIBU2Bdn3a8dPPcjnX5FCRk6Eo5sT1mflmLYDTrrYSjLdGgUaG4pjQDm7kENVJVbc+2WJPJw80awBvNunwpBaCGseYFhnhPVQI5SAekBYZEHkemMtU0lSgvXAMQNoiW5hGg0S4QLikDQ1ZaACQpJysEjgPmHUJo5hKBU+ZFSYyiSVgEmOyZWmG8+bu4iy14P65EYQJNcVjpZeTOw0975DWXQ1+ioA5SzF27YeRQ5tFFEk71XTTeYNBBMnLaqdt7bPJ5QmNzWc6WqkpnmadDgiHGnbkPV4vP0GM9LBiLjMyPOI4WxCSYQ+ENLV1fGaosyIxhBr4UgrNmeuh3xrYNUpYYW5KAUwN4bGvmOCGtRD3ocbMz34J/WKxw9J7o4/m3GJifq/2wU7sHUvOQRcGqQF6rgEURynQFoE3JCcJvkem2N02CzPAorWgAVAldDlf6z/VKuTN6mnW+uz2Civ25oDrOr60fePQ+eQfQciFG05RFnzh0NjLMyYq/MgZBurK+2BeRR6e9L1BZwJ6PVm7fprxFj66M96OhhA6xGcN6r9rzG+4UbKDNQnPvEJPPDAA7j55psnv7/rrrtw6dIlPPLII3jlK18JALj//vtRSsGrX/3qrZ85m80wm822/g7YgEnan7Myf6zGB/JvVmjpWLBpHr4eiBgDjrFr2r+b73ePTgcB07qCUP9mCqLB13rzITjUgOaQbr0aiXDEa6RRNm13OCLur73IMfdxsg5VIFaux8QixXjoQZAjyHirPilM6huOL7pEAWVT0ihAvK6N6HTa9rxGmKx5rmmtWXP42fyDYeFTaG3bdU3vDeCMyEj12lQNxL6DjCW1AeVuHebx26ECXfJhPEa7Z9MxJFHqbrvXtnCSd85V0oJ8R91H1qCwVeSWQwA16ghyaFTFbdReYe3haXPUexTGggKhdXOA0M0dVpquJwHiALbr7cbFdb2nv9fcsY8AZ4RtXe9NZm1TYM2orzedPWktA7+H0/vUGMFNFQrm6Vzaa7Y/zT7ebOQp5CLdO5vz2Fxr2ytxGlXYWk2bYrKSHDD5PimCbK5ty7BaQ3dyDDpvYXpzMK2DQ7Nek3/br72OautXorJVN9ZHz+VJ+kCdKGEMX+sDp+OrNlL7+/v45Cc/6f//6U9/Go8++ihuuukmPP/5z8df/It/ER/+8Ifxvve9DzlnzzPddNNN6Pse3/Zt34bXvva1+NEf/VH8/M//PIZhwJve9Cb80A/90FfP7ANAY0Y6HBCGiHA0HvemdHN5krFw3SClyMFCpMrNNN2YgBgze8D09Q4RUXNIAH7IeHM0/QhpsV49RN8MBknNOow3n0JeSEPFeCBJYumpkzyKSgfadHAWnfkELaQkYtXiqhtU5gtpWmYtMMww2DIxpDNul9xAhkGFbDMfp8H6BxuUgqqykAswQuo+rNGfQmyiei5kFGSJhsKhQmVyo45/flelcaypHAC51vlc55/rPVODCcAbIAZ9IINHcno4hYaBmQK4NPBg453HoxHe88iKYTXnIpEB9PVNXq5OAhwj8lw8aG669cbDAVjVmiUxchndlQFxpaLD2tnUImBj0ZkyPBUhTXg7D8sVRMLYk+aakuz5CSwKlWFiGNmHtW+Y9JYigcctjxE2notNh8PulXntJp5rz0EQhliZiwgqFVHK2CwR8WiaCfEoo1fnJy5zdZYKASie57K97Hm5drAqUWgOzjX9sqqeJ3g0ZffB/6TGsWrvaC4e+Vhdl4sptyQO5rpfms82hzbP1CAVEsWZove4jWqAY583uc4WppvL/i0qL2VpkNwFZXAWYDlOnUVUhAIgUYABKrnBosTGSMt9C3XNgdrfa2O4g6bRozgLBQZtf9MEZh9++GH82T/7Z/3/DYa799578U/+yT/Br/7qrwIA/vgf/+OT9z3wwAP4ru/6LgDAu9/9brzpTW/Ca17zGi/m/dmf/dmv9lLk4RlGxMtHCDF6R10imhr9ptAPzPXQNckhy0dsNklSD97o16QGSvT8Go+ODJaxpmBmnTSHEWqHTulaKQ9f10WBHOczLJ83w+psRFoWzB6XJnoIQLHul/trxKsCJ9GpOXC2V++8eJV3WBek5fTBco21pgum54AYAJRiOovgIoyzsFYsPrOsATcP74ZHxjEIhZdY6MBrbY6YtdaoNA+e1h+x1iMFO+BSFPp+G30FYRWVmem3oRaM9p0cMt4AsEwcBxCJJqLCbkHnYYQIkYyqxggapIvyOHnDyW6/aQpp3ZrbtUUPzJN7jZtyMVQY3AWMe/rAc9Dkv9wPOlrbHRFnaFXQDZUVVxadqFJoEeeERQegzOXxtcR26WyviRSWwL2p1gbpgRGGgni4BooQIqxQmpiRDuT747LRAiyohkqfIR+qxlFmCcNpaWcTj4IoxVt9TSnStXqRkPsggqpk0jxN4bERICC0c+ts3dbcOUNOdIs8UvDoo70/aoD8oNeIsjLMkkCgg87H4ClARIktR9xIgNmzwYkwLpIyMRk0M11JzW2xPkdHwyTfJCw5wmhGKgeJjseCOGhnXkDJYAq3WXdnL1+oRdsAi/FXKLHmOBm8EHUOcbaiOGybIwBgLZux2sJQYVJTlLcSgzCaCjsQBvm8sC7HDI7tqzyTzykz63GliEhrdJ9kfNVG6ru+67uOe0DtxV2Hdbzpppu+6sLdJ/jCCt20k96MAK51XbZYxR5GLTqzmqXWQ29D+mZ4Tx+t45l+V72BE0YL4RgU4PRiZbg5jGlDVRwM7ydtZGfebMgFYWjzGVRrWJzZ45cFw7wnMGUDF7TXPYFSaLoW5hW1DeyOrfEmbGhV/IE0FGgOwCIPmUMqUK8sQ/JHlgcKVA8QOn5/JGqu1F8E3opYGLEDarwsGrHrqV5x9WTbPIdRcrcOe+gjREU9sHB6aPoaH1bYCrhxl5xgQ7HWtZJcHleG1zZYhuDRqu+xOF2nOhF4NGBkDs9RXMcw6NbJQ9EinMrokrVjh4fJxJjbBL7BzGp1tirt27C9uy2SAjz3KMW8+mweY2I0723JHbD9HaayR8wAjDwBuHJIUsSCJb9YSkDUTt41x4lamA5ggsIwaiHstmt7guF70I5Ars6zP0dOCNpurGSfBr9/25bJCu4n5Ca0EZl9XkPCuNb22XYdW8aJ1u5r6ckMO1xJvKAYRTonhIkKNscA6rp6mHq1O4OGUSEsVTQgQtmbT9lEROKdqYcYDgd0g3j4tMreU8hHCDVqIXgztbgWyIH35gARZo8PSIf62haSbK4bfQcuRZOUqHkMrTeKhyPItPjUE5ImcBrNhHq4mtDkBEooDKQIalg5PogquUAlXiTaKkj7tbmkwCgMdKmqe5hxVbFY6hpVci3u5NAcwLp+eZGwvElV7dcRQYko1rE4rjLS41HIMERyXdtqL+yQJvJIm9YZQBYvT3vxmDI0Fcg6xoAy7ySSYQYb1KswbemTFoCaIxMkeeyincUJDFaYa518vX4MjdEwFpbBTwWVITkXMgitx9qaHhJxQCGkqHm7uEoVDi7s0bjscztoqvEVckyY5CA5EPIZCTFpyDWSa6Em+6PRcloGN/g8j1rfVIAiIsohF9HtY92fnUTIYd0ofDSfT0NjGIzgZM8isPW6/TPaQeokWAsSQFqpA7LWSijgIMzNtpeYOXKWM7J7lWcRw+mIcSaGp3Tia8UV0B8U7dYMxLXs9dwH5HnN3UqHWiHCxJXWJxGUiUpe2OtOhRnxDcq/3Z/YrJ0RFsLITrgB4CSkoiiCi8wOKg2nESL02UKTJmHLCetzVDqbDyH0AUGJIELisSgR7hyHFQtrVdXly6xD2SKbtm2cbCPFxWElGjH1qFOsLRwmSduGRTeM4LWG18oWIyIxbjGAZz3K6RnKosIqABCPRsQrS2DMCKvBNe0wjuBhg7ESo4iyN4eJQ0ORUE7PROLly4foS0FZdBjPzpD7UD1aBhDkUKRSPBEvF8XOJqOcRdWACKz6aVRYafcZ3owuBDnsDpZVzdwGUcX7Z50bIw5BmtORtkvQ/EM8WIOWKzGA+noGQH1XWZONevxExDeXyRwAOLTDIWBcRKzOKwtqENjS7wMB3WHE3lCQjERg+oeTPdI80EX/gD0nYw0nvaHfWls+AFobkpTWq4dDqIW7tShRD68YPAdqDCxn+kHZhXo/OZJoBxLc46Ss/cr0QHRoy5TCgxTK0tG6FjQPzb7V+xf3I1InzNV8ukdebGj62TroIRR4mEYJBeC9DsNpaaGR9gdxwPTzLSIiyweq2khYqdE3Ne9QELVWD0GijEBqAJUR25Vq5ASO1etoilVlnZscsEZsAi2r7l9rmBr1DzQOF+tpZwxRuzeeV1OxXbhGJk3XBZp/YYHt16cI4x6h9MCwB3AHpEMgXxIpMoFb5TnOnbwOkLYZ6Yg1PSDrVp9xEwZoUJdQ87LhaHApLDe4q1GEAQDwLAGzTqNYdscOgLcxqooy1OxV8oLdsC4utGzPvjF07ZpKRxgWohUYMiGMcm/yjLSbNNAdCluVMiMtc2UxkkDVxRi6TzJOtpGKsSqa2wj1YfU/CkHYcC+ihTJc1qR6msaAAyz8bTxy/zB2qjW7B90e+qF61lDvwjucNfCGi7KmCouAAY2WvcpbJ2JV6luhNTMORsu9BlxQcwHNa2KTkG7XcQMom/SaKUUvbuO7zOO29W3zWfb79lo21sTaikgNj3rgBCErBGhhYaj05yeDD1r7ZUwph3XluyZKD+a5bwy/Pxt/ts5Nr9tp7xbJmHfffk/Zsr/atdyMbrllRWr0zQyv7DZ/wO9Vfd/xSTWGHIDX9kRopFivD7D9x5P/b3s4OVTUPDcevTGACbtnwxhsruG17muz33xN2z23+T5zYEz6TCMVKs1+hz3raCDf+llFT8zSEawTQfG/WZCSgMqO1Ke9JHi3HrbniVHh9U3CVwOTtWvpf2977oFpXs4+W9fHGYQKV07W0B5h0l/ZzzaO18lXGkTI7I1Sr1Wga2taC6iBa8KAG+NkG6mbz4OpE4aeQn0GSVEWCR5nJ5mB0SaGrEoT1ElEgNVa5EDM21fjF1QzzpLtEqqXGq0BXm0uWnSYGEqKVudENXy2nFJbJR8C0En0I3UU4umY5yZzqmy5pHAF7FoiocxEsmXSwCxK5GPDNlEoQVp3sz4pGlFxl8B7M48YiqppxKMBOLJiVfHovdI9C1OLhlA3tylETyR4UEV0FZ6Drt1Eb1EhwG5/xPxx8UTTQUZaZozziMNbOgynBJbIi4i4NPX6rEWkmEZUusbcVxgsQq99olQghIG4v3LP2hwasgaIds0aFZc+oESSxnV9gpF2qDk8w1A8CR0GISuUmawtsbLXFPIhzZ8ce9hLw4YyWHPiTAWPVMdzC4xnOj8UzEiKUjjXvKaNFsJDjVZLJ15xWAUkFeZ1h0ojIF87a+6oyhpWyF5SQJgpNFwYWJdK99eaIO5lPpSzF0JbPdrEGWL2uRzLc9rzBYCpcVj0fWzSVQTpKjwTkgcNGbDvbIvKe4s24BAwR2B9JiH34iDlXo0cS6RPhfRvWcY2V8kRLjXmDSmhn62FxeYcs+ZduQs14lXnxtRtPG9n92wLCcGUQ/zfQ5HSuC7KI6hs003HUogTBhfrdWW5blOmNwPLQDXKDMQ1ozusdZZ2n/NcSBQhS1qivbYnGyfaSI3n5gg5IVxloIxV5igQkNfg5ap6KEEVDxZzEaTVTc2AwBXGICLxyk1c1ARhOQVw1s9XejghVKpqKcKkGjQ/ExoGYOOl0ZC9s6tBbZyE4WcSTmVGGGfBCzbloA8IYwBGQjhYg/aP5PdJ24woxTcvTLJG6a9BoQsV0aSxPrRIUYxLW/kd5XAoKTgE4C0FTEzTmHYKmZoILCvte3KANZEmADdQ3DgCoo9YqvHS9Yz7a8yVPZUuLUEHS8Rzezh6bkKeS2uJPBOIJqyzFOqaFmKqPaXafEKZNQzGjQibWPIc4eqRXKPCl95FeagUXiaIA9DpYT4ERKPL27rY545Cww6DGqNIwnSby2fHo7GqQdih0UbtLIbFnQ+DbE0VAfAWJUgB45kOy+fIo90dqvioHr6uutF+j64NZaA1UrlXrcfOWo+Q7J9GBsv6Kcm16HorocNgzTJLzjqjIp2o5WtlbqULQAqIRwwMK/nMPomKRgBIjXsbLRyjMLcRmK0bmoMyos6jGXGp+Rx7v5VpaMQOALQWY1K6gPVpwnCaJgYIgNTAZbiR8qg/1M8qSZbXRYIBJzZ5AXQu4ggtKl2fWjWLEMCdUMO9OzWmSIStShgaVqSqxshZBaBjd4raNbMcWElBC4BZin1LAVmrIoMJ1XBa9CgQX0F/RfZ+7k0tByp4IEo58UgJXddAeDbHiTZSx+Clra8pMB0/K470A8A1vfg4g2nTk7VIwPtbyOJ7lbm9ZvIRVNXDaeN1zR/JWTTvG4EQNYFvm8FyCU80Zw2hGSTkB3tQqG6q6pU189wSoovOmXxeCy0aY26yTtf4jCcaYoCf7DWqH1e4HrCuf7cxh032pR1iBgFdi323OVoI1ei8lrtoP9cPff3a9rzUqBioRgZF6cSlOGQ7KcLdMBJtmwr5f1TDwtdYf2PVte/RyMObA16rONvgx1hrZQSKglPMKQbtSYRr70E8we8s+qZas4c65ev/zABh39JGQX7r5Nj9thKoDc++FeD2ImSD2MgKq+XanPySRYqMRj1S1Agdv1Z9n/6z7Yjd/t4HkYT2rChOQw7hzTluXvO1RgvhFzgy4F+pz9TWay9PcB/QnkmA11Zb1F6E4WzMYmfn+j5vz8An/BofJ9pIxatrBGoiAxvG+iOS0J/Nk4/uNbYHGgeAQhDCQIweRU0MVwEIGdJtNXg4THmQJGwuTr4AEajv5KCy0N08Rx0Og2lrCUA8mLAaMfsKOxPPewStikBuVqkdpOvlVgNB0EgIHj2JZh7EI82NB2XJfuulVWpjNdPVg0ISm430HPI0aM27Cxc3Kk94YOXqkTuhos1zrEekfY0kjFnH0owvHUlrCTLKcwxCLIna3E7VH3jWASq/5JT8dv1BEwPjTD1ohOdtQYrDkFhJ8zgaojyQUVuDmCL2PCGfnfn1imG15nGjsE9ZO7qCkPdE/ioM2gBw1UQqBrU4sSTXKFbV+yeGOGiN14HAhmGlRJDMTQ2bFrCaPJUlzWcdhtOpYXgVxJXkXw5esACYsfjygP5C9khxk4Xq+6Kpq/IoyqFu8tq1zRyb5YkcGofsWS9Q7atSeQgABlToPBfZC1acbKQMhYKTQa3zJGtu0YxeZ1GCCggqoWXPjNz7sJI39PsB45ywvClgXNRnzqDJkOEEmVZt3QxaHGpH4xJFWd2KjE1soHR6Zth62ucrZd8jSUUs/D4wC/Qa6/ojErJKrIEZ8Ujr/5gd5jeG5KTLcfu8W2SqDnVcFXRaWmGwMBUgLgvi4ajRY4/SydqUjpA7mYSxFa8pxLsxTrSRCodHoMjVqADHD0WTEymM6lY1jBUylW81Uu6JTQ9/PyxIPMGicFI4Em0/ZwkC4maZHpxTk+m4x9huAnOuh4x0sAJyAc875NMzzWdkb2s+oQJvOSRaZQsaC9K6eOhPK6U4Tzxw1oNZ4by1qHBwpKpSrrCeX795reE4hELtvdhmpOzwsj5dLVQT6mtoNUwYgPYgSr8medglWtQoYhaBHIAyIAyjPIAKhVr90yQitXvQrJtBkaTKGCYy6qMU0GoQev8QENfjlPYegNJHDGd7cADSYRalECtqtgJysY/gKLBISYS4IoSlwrsbShzkRoodbuMYXTy3fr9AculIXuPFmOZoGGy2paVKXkSszksr+e6QMbskJQ3jIuHoZnlm4prRf+lJUIcNCNX3uTH22tCzMVST2qyiDpXuYROs5dhpqxB4gSmIJwceK8RtOVxkyMG91sJaLFDm8di+RSBnecaVMtwYXqMmeTOJDoazHVZnaauRdnHgjegtdHovVaJMCBvwgnUaQ6WiB3m22Zww3lhme7a2RD1W3N8Slcpc9U1HFUJerY+9rzpUFiltPL8O8QFhLEhH8szkRXQDHVcFdDSIs7jXOSJQIqSd/WiW9/rHiTZSPq51GMZYyQzMTuWsyV/A8h9WT8UxTOptSFUTnHqsByKNxQkTTpBQQybfLXmRloBAgeXmqVdFKUpkpg0XPTobGgNCBGuD7orGOTvl1ARfpQPmxrIQtI091/oKwA2L1zqNmj9rCxaBGnUW8roxAFX53KGsjXUnqmtYCgijt6tAtFYpx700v5dGdtkwhhJdBqk/WTGiJantfX7fCdx3UvjbS08oj55bA0UCr7iu2MiCxXcJnBjIEejVc22pypuFzcAx5qJL8DTXxVFEhVkjKaPUux5f0eholqaOVEGtndrY507Jtv8PhOFMwvqMajjqYRNGYH4xIu6vXQnC1ReSOlGFxfATqtSNRRvKhpSDNUgdol1PA2eLQkIRAeViEG3xPcIhTPJJXNRJaud2bI4EMofS7kWBMxCRGUTV6Hrk1nzv5F4Bftja5/uwvF9UNXlW58ccIUM4OjlsN4S/5dDuAWsHFEa4Q2LwodcvWXmJXo/D/9DnYzTnuMK1RrraCvtuGw3kzVFau5QuIvTdMWfFCC+OvLRD77HpJCKLs04E6codbU3Z1TLsuRIEA14bJnR7HDuvrjVOtJHirgNTqIn3dsTgHXMpZ2Cssjt+wGfRmOMUwafmyDdJY0RnwK1H0P4IWg8opxYYz8yQZwHpcER6/LAmxxXao2FUtqqwDC3xPpztMZyOCANjpvInAMC9ejl9Qj7dS7uRtRmqjDITNhonKY7kIPPpL6+RTJm8FNC6gFNEGHuBmTUiMIHIsM4IyzUQhFzBISDPE4azIlMTVwXpMHuBbzgc/AArc2EGOhmACOXUDPlUBxSVz2laUwBACVG+J0oH4HDlSNYmRYGnQpBk7Go9oS3XDygWsgpklyJ40eu1kzTbW1ZB1laGCsxSw7XoFNqJLksUVwXpQPUdicCzBI4ReRFVnJdBY+cHlSXaKYvWnjXrc5maGJxBKbVMyQ91KXDGxDEo8wDsiZwVDQX9pcFhLYN58iyCT3XNnMQhioeNtJc5B/q97QGV5xGP/+EOV1+SwV0BuoLQFZT9Duc+tsDZz3TqTZeJl280//mXB02GByGF9CLb0+9b9MvgRTdpzzLRqcsZ8UoR9LZhrIG0rqyjygRkzbppfskU5O0zfWdEguks0igtToxtWxadRlRZcmVjRjiCFOu2+aKAWoweTWdPDZIyYkXlRdZ3OJ2U7EBYnxFYT+AsqDNDinBMty4nYPlc+Rxpu6L7Zw2tjQL6qwXdFWEeex88RiW1QB1VWCTaaAC2EbXfAGpKR5qiZ90/Et0HDKeCGoleco65aJ3jUGFAi7T13pm8URvt+ncrLB2Pxkm6JZ/qwYmQF8Loc4h+pd2eNV0xhQCuPU60kYJGImgb4tmIvcJtdFzNPGt+ZbUGrwfQrEd5zmkMZ3uYIrezTwy3BTDuReRFQBy0p9HRUjr2zvrq5Y1VlaD08mfcixhOiWZZdzUhHmjBbaeFg70clKWTzR2GRuhRG42VXgogASCuE9IlqOdaXEy1paG6DA30EFwNkp8JwoYb9yJW56RiPq3EMwwDozvQLpujbKTSyXeGFCTyI2FrjYvk3x+Hjc2WAvJeEt2uo4BuJQ8B9x3KXKLVQIQwjKLBpsMN1qb6AKBrlGA0cekUXItcJ8SFFKSLcZJDdtSiQyMNmJdovcAEU5dFy4sAYm2pMBccPYxAHwlhlRFUcNMKe71wNAZpkEjaqkTbY3gvMGNZaoF1PBxqR2OFcHnWIZ/qMC6iR0CihYdqmNooUP+0kV3uA45uZbzoD1/Ac2aHeN58H8/r9/GxK7fhdy5/K/rLEXFgpFnQ9vB2IAnTLV1eIaxH5DMzrM/PwMauXMKvqXRBch623oURVgyUrHm72o3Yr8+gyRhqftdrFVGjCKtXbPdTm3cdCyhLXk+eryBI9SHVvbMuVe0kNVG0S42RR7q2X9p1tCah41xyLcvnEtZnpU9bdxUKc1GNjppRemA4zeAElKWSq0bJwcW1RKpxmREO14KuzxMKqgF2I9VElVvVNFqoryUNWU2bOeRq7I0yL7mvACBJ3k3TCA7zDzWSBODIh5diWL7f0g4FFb2LhLzXIy8kr5lnASUBVLQz9Io9n8qRwM8GIyXJ0S3ipED1LkhdRLLFN8iJpa4nKORjdGvB8OQwTgHRxE8DENfyOaR/O3MPOA43NmwqUhZYyHCqu9BvgzC4NCKAelQCb2hHTPt4qonXkiTJLTm0oR6YMK+WJuyp0kfg9BysD7a1CxAxVaBkcsYTt+to0klAVTBXgoUlVjlUWMSnroraQRuwtRCby81sJs2hEEmGz8XXtZjIpVwXG0wXg0c7YSziSVt9Thf8wcyWvO31Wg2K0uuX/Icc1DQCRkOmTHKpY91TXrRr16gin0QEikW2WyNdI4eYkAUoiDSQF/jydG2gbMZo0lUGGZpRtXscGomeLaN0jOct9vHc2QGe11/Fc9IBzvXnBcZTiKlEgGckSf5Vs1e3FaQ31xLGeu9dWLeFOjV693tHxvKCQudUD2C7/21esCUAaB5qYqQigaFCwak6UZxCJf8050AL5U0iDFt/yN4wAxlGgbOsQSgAhFEUFcwg2TlRkr23gbFYXl/AFWrU14Qs+0sOeI1UNiC72lakrtOkS/C2tIZPUGqsisLFIligXYZLAy8r3OZn3mb01d4b1L062bM2Nt9D8HNH1pP8u4MydX3/PAmL0MaJNlJ50SGyHNakSW6RO9GiVJWGCczOjsJMYCP3qllfG6W6nikAWvsaIyGMc+n4C6B7fIkOAsVxINCsb6r8ddjN1cLEkFk65wbV0lqNtd1HCio6qu0XAK19CsiRBIYAHNMu2oph3IuI52eijXYYEfbJvSkalNlDQZ5VAtbPmQGkrS30Wse9gOEUkOcEEFf1dIJswjEjMIvsE+T/aRjBJSAsRxHOtChw3hgphbjCUEArZZSZfI4yr0oXat1Po2IwyW80P3M5mAKPPE1vb9yTCDQeFcwPBoT1iLJIGBfB63zGhc5LLsCNpz20ITNml6sxFmMlEUEEnFprUjK2rybyOuuAsIrVkBkRZize/JLG1OQdc4VN1CjQWJCuLKsBbHNyul84kmjxNRI1m5BpPl3w/z7nk7i1u4SzYYk5Dbg87uEDEKkaDsCwCCgdkJaM+dGIeJSd+Qlti+LsN0v0M6oOZFGYrNN9pqzIa9a+GEzawrNRGQEtVO/Gv8jec5gwKsQXgZlAc+OpiHERtLVI5146aWNLqK5iS5KydQ0GUVt+Vp3PqHqBYS3dvnMfMC4qocLIDpyAcS5/0wikpRkgqB6hOJ9WT0UZiEdF+7nlreskhfPqiCgbV9iJ+Xjet31/MesozMX1+V6hdmHaAXLfZpcBEzvOfUAg9ho2+dJGFcJyX3bvrNSmgQFd7ccMlXWT0GsKK60NzEA6GCsRxXJgT2Rwm3GijRSngEJRWj5khfRGpdXE0ITxFe5zmf7IiOvkxZ8uUR+qp0RM0s+o6GF7uJKD2npJKaVTPriJ4kLQxKdSaNcFoS/a16kKrSKbh8ma/LVoQ9wRVxG2+er1lcQYF1FpvQkhNYWjShH1zzJsuLPkJTumnntCngFhIJQoXn/bcoJWw7H5ESBwTpa8UdlLLijpnmRmpNUghmoQWMeuyxXeW4hi08i3RkqJIRhGQYW6CMzEQOU+iFZYR942wKDZkqBGSiIoJqjgZ0BJ6jEHPXgHEfm0NbYRPEFf4R277wRhjZFBzUSN8yHRtxV5u4hxO6+mdYwPE+HdfHhT9C7GQrWXaNh7JLWDAJpn/D+zL+C2dBUdCiIxnttd1b3IElEmSDsPY6Ep1DRRooAcyGFoUYFSKfApSDrJimUiiVL9tiQ+m/Grz4j/fBPC9DXK1ZkDYGrepj2Xe3FEAImcgzL6hIjRMmA3PrvJ8TCKlwRMYLUSZf/miLiKknvSiMgaQ5YOKD0jgpwuThqVew2c/k1FkBhXWmhzjnboN8iJOZlhBNA2JnySwSlg3LPWQ5CuCPpM0rIIlDmP0lk3QfUpw3R/W9QV6jWiqd/z0ayrE3iozsHaA1GBFMmbvqA7Itc3pxNppCxEHscViAqCioVSHkFlJWFvBopKeMihqkKUGSijeFqc16CyAueCcVxiHPLESPFQQOOIMI4iKZNXoDJKMebmAcsMKiNQRkjYI40DmQLyCIyjePAhrxDKCkDQa5Q7lcdUizx9noSi/VdyCshrza0MtciVxgHIKwDyWWVUDBlB3y9qCBlTIzUOEXkVkAMQ1kAeirB0xhExr4A8YCoZZA9wQMkBedRE/5jgde9mpAoD46BrZuuWUTJhHIN087XrznljDYvkNiaDtJtyBoeIPDLyGJEpYBwCChHGoWDMS4S8wjhG+fgQpLFoVKmiNWMcrChTHkSj01ok2xop2mKkaGQEU3XPWe5lmVL6OUe5VlIRVsuX5uIEAD+w7U/ZXOd2+hGcCxiC45cQ5fTbYqTGoaAcFRxczdhPxY3U0eGIspQ9XoiQ1xKthzVjHAe9XxkhC+2/ZMI4AIVoYqQwattvZnDOyGPU2iDtSpwZlNfyLAAwN1vyD7GGZ45dl7o+W6JoWc8CzmN9HnRvj0NCTgQedA55DeshJeSbAA5RIHU9gOU7uTFSWktp91vXvpAwXvMYkdcFeUVupPyyI4QivibQCuABQrbImBgpYiCvGeO4RhgzOI+yzgBKLuAxHDNSDq9mycEdY9ttgWTBhHFMGFWGC3p+ebmDpgzyEJGD5MnCOILHcWKkREIr+1zbKK5t3+OacEQuB1coIsfpOUYFwLiWzwTAeu9HPbeerL0T8fU0gHqajf/7f/8vXvjCF97oy9iN3diN3diNr3N87nOfwwte8IJr/v5EGqlSCr7whS+AmXHHHXfgc5/7HM6ePXujL+ubMq5cuYIXvvCFz+g5Art5PtPGs2Gez4Y5At+8eTIzrl69ittvvx1hWx84HScS7gsh4AUveAGuXLkCADh79uwzepMAz445Art5PtPGs2Gez4Y5At+ceZ47d+5JX3Nt87Ubu7Ebu7Ebu3GDx85I7cZu7MZu7MbTdpxoIzWbzfCP//E/xmw2u9GX8k0bz4Y5Art5PtPGs2Gez4Y5Ajd+nieSOLEbu7Ebu7Ebz45xoiOp3diN3diN3Xhmj52R2o3d2I3d2I2n7dgZqd3Yjd3Yjd142o6dkdqN3diN3diNp+04sUbqX//rf41v+ZZvwXw+x6tf/Wp88IMfvNGX9HWNd7zjHfiTf/JP4syZM7jlllvwvd/7vfj4xz8+ec1yucQb3/hG3HzzzTh9+jR+4Ad+AF/60pdu0BV//eOd73wniAhvectb/GfPlDl+/vOfx1/+y38ZN998MxaLBV72spfh4Ycf9t8zM/7RP/pHeP7zn4/FYoG7774bn/jEJ27gFX/1I+eMt73tbXjxi1+MxWKBP/SH/hD+2T/7ZxMttpM4z9/8zd/En//zfx633347iAi/8iu/Mvn99czp4sWLuOeee3D27FmcP38ef/2v/3Xs7+8/hbN44vFEcxyGAW9961vxspe9DKdOncLtt9+Ov/pX/yq+8IUvTD7jKZsjn8Dxnve8h/u+53/37/4d/5//83/4R3/0R/n8+fP8pS996UZf2tc8/tyf+3P8C7/wC/zRj36UH330Uf7u7/5uvuOOO3h/f99f8+M//uP8whe+kN///vfzww8/zH/qT/0p/o7v+I4beNVf+/jgBz/I3/It38J/7I/9MX7zm9/sP38mzPHixYv8ohe9iP/aX/tr/NBDD/GnPvUp/u///b/zJz/5SX/NO9/5Tj537hz/yq/8Cv/2b/82v/71r+cXv/jFfHR0dAOv/Ksbb3/72/nmm2/m973vffzpT3+af+mXfolPnz7NP/MzP+OvOYnz/PVf/3X+qZ/6Kf7lX/5lBsDvfe97J7+/njm99rWv5Ze//OX8W7/1W/y//tf/4m/91m/lN7zhDU/xTK49nmiOly5d4rvvvpv/03/6T/x7v/d7/OCDD/KrXvUqfuUrXzn5jKdqjifSSL3qVa/iN77xjf7/OWe+/fbb+R3veMcNvKpv7HjssccYAP/Gb/wGM8vG6bqOf+mXfslf87u/+7sMgB988MEbdZlf07h69Sq/5CUv4fvuu4//zJ/5M26knilzfOtb38p/+k//6Wv+vpTCt912G//0T/+0/+zSpUs8m834P/7H//hUXOI3ZHzP93wP/8iP/MjkZ9///d/P99xzDzM/M+a5eYBfz5w+9rGPMQD+0Ic+5K/5r//1vzIR8ec///mn7Nqvd2wzxJvjgx/8IAPgz3zmM8z81M7xxMF96/UajzzyCO6++27/WQgBd999Nx588MEbeGXf2HH58mUAwE033QQAeOSRRzAMw2TeL33pS3HHHXecuHm/8Y1vxPd8z/dM5gI8c+b4q7/6q7jzzjvxl/7SX8Itt9yCV7ziFfi3//bf+u8//elP48KFC5N5njt3Dq9+9atP1Dy/4zu+A+9///vx+7//+wCA3/7t38YHPvABvO51rwPwzJlnO65nTg8++CDOnz+PO++8019z9913I4SAhx566Cm/5m/EuHz5MogI58+fB/DUzvHECcx++ctfRs4Zt9566+Tnt956K37v937vBl3VN3aUUvCWt7wF3/md34lv//ZvBwBcuHABfd/7JrFx66234sKFCzfgKr+28Z73vAcf/vCH8aEPfejY754pc/zUpz6Fd73rXfiJn/gJ/IN/8A/woQ99CH/7b/9t9H2Pe++91+eybQ+fpHn+5E/+JK5cuYKXvvSliDEi54y3v/3tuOeeewDgGTPPdlzPnC5cuIBbbrll8vuUEm666aYTOe/lcom3vvWteMMb3uACs0/lHE+ckXo2jDe+8Y346Ec/ig984AM3+lK+oeNzn/sc3vzmN+O+++7DfD6/0ZfzTRulFNx555345//8nwMAXvGKV+CjH/0ofv7nfx733nvvDb66b9z4z//5P+Pd7343fvEXfxF/9I/+UTz66KN4y1vegttvv/0ZNc9n8xiGAT/4gz8IZsa73vWuG3INJw7ue+5zn4sY4zHG15e+9CXcdtttN+iqvnHjTW96E973vvfhgQcemDQCu+2227Ber3Hp0qXJ60/SvB955BE89thj+BN/4k8gpYSUEn7jN34DP/uzP4uUEm699dYTP0cAeP7zn48/8kf+yORn3/Zt34bPfvazAOBzOel7+O/+3b+Ln/zJn8QP/dAP4WUvexn+yl/5K/g7f+fv4B3veAeAZ84823E9c7rtttvw2GOPTX4/jiMuXrx4ouZtBuozn/kM7rvvvkmbjqdyjifOSPV9j1e+8pV4//vf7z8rpeD9738/7rrrrht4ZV/fYGa86U1vwnvf+17cf//9ePGLXzz5/Stf+Up0XTeZ98c//nF89rOfPTHzfs1rXoPf+Z3fwaOPPup/7rzzTtxzzz3+75M+RwD4zu/8zmPlA7//+7+PF73oRQCAF7/4xbjtttsm87xy5QoeeuihEzXPw8PDY83qYowoRfqNP1Pm2Y7rmdNdd92FS5cu4ZFHHvHX3H///Sil4NWvfvVTfs1fyzAD9YlPfAL/43/8D9x8882T3z+lc/yG0jCeovGe97yHZ7MZ//t//+/5Yx/7GP/Yj/0Ynz9/ni9cuHCjL+1rHn/jb/wNPnfuHP/P//k/+Ytf/KL/OTw89Nf8+I//ON9xxx18//3388MPP8x33XUX33XXXTfwqr/+0bL7mJ8Zc/zgBz/IKSV++9vfzp/4xCf43e9+N+/t7fF/+A//wV/zzne+k8+fP8//5b/8F/7f//t/81/4C3/haU/N3hz33nsv/4E/8Aecgv7Lv/zL/NznPpf/3t/7e/6akzjPq1ev8kc+8hH+yEc+wgD4X/7Lf8kf+chHnNl2PXN67Wtfy694xSv4oYce4g984AP8kpe85GlFQX+iOa7Xa37961/PL3jBC/jRRx+dnEer1co/46ma44k0UszMP/dzP8d33HEH933Pr3rVq/i3fuu3bvQlfV0DwNY/v/ALv+CvOTo64r/5N/8mP+c5z+G9vT3+vu/7Pv7iF7944y76GzA2jdQzZY6/9mu/xt/+7d/Os9mMX/rSl/K/+Tf/ZvL7Ugq/7W1v41tvvZVnsxm/5jWv4Y9//OM36Gq/tnHlyhV+85vfzHfccQfP53P+g3/wD/JP/dRPTQ6ykzjPBx54YOuzeO+99zLz9c3pK1/5Cr/hDW/g06dP89mzZ/mHf/iH+erVqzdgNtvHE83x05/+9DXPowceeMA/46ma465Vx27sxm7sxm48bceJy0ntxm7sxm7sxrNn7IzUbuzGbuzGbjxtx85I7cZu7MZu7MbTduyM1G7sxm7sxm48bcfOSO3GbuzGbuzG03bsjNRu7MZu7MZuPG3Hzkjtxm7sxm7sxtN27IzUbuzGbuzGbjxtx85I7cZu7MZu7MbTduyM1G7sxm7sxm48bcfOSO3GbuzGbuzG03bsjNRu7MZu7MZuPG3H/w8naIt3rAQ9GAAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# Fetch one data\n", - "_, data, mask = fetch_bbox_array(sceneid[0], bounds, assets=[\"B02\"], width=128, height=128)\n", + "_, data, mask = fetch_bbox_array(\n", + " sceneid[0], bounds, assets=[\"B02\"], width=128, height=128\n", + ")\n", "\n", "print(data.shape)\n", "print(mask.shape)\n", @@ -1125,43 +578,11 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "900812f792d44a349a3b2e1579b17338", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - " 0%| | 0/85 [00:00" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# Let's fetch the data over our AOI for all our Items\n", "# Here we use `futures.ThreadPoolExecutor` to run the requests in parallel\n", @@ -1170,64 +591,41 @@ "bbox_worker = partial(\n", " fetch_bbox_array,\n", " bbox=bounds,\n", - " assets=(\"B04\", \"B03\", \"B02\"), #(\"red\", \"green\", \"blue\"), in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", + " assets=(\n", + " \"B04\",\n", + " \"B03\",\n", + " \"B02\",\n", + " ), # (\"red\", \"green\", \"blue\"), in next STAC item version (see https://github.com/cogeotiff/rio-tiler-pds/issues/63)\n", " color_formula=\"gamma RGB 3.5, saturation 1.7, sigmoidal RGB 15 0.35\",\n", " width=64,\n", " height=64,\n", ")\n", "\n", "with futures.ThreadPoolExecutor(max_workers=10) as executor:\n", - " future_work = [\n", - " executor.submit(bbox_worker, scene) for scene in sceneid\n", - " ]\n", + " future_work = [executor.submit(bbox_worker, scene) for scene in sceneid]\n", "\n", - " for f in tqdm(futures.as_completed(future_work), total=len(future_work)): \n", + " for f in tqdm(futures.as_completed(future_work), total=len(future_work)):\n", " pass\n", "\n", - "results_rgb = list(_filter_futures(future_work))\n", + "results_rgb = list(_filter_futures(future_work))\n", "\n", "print(\"diplay all results\")\n", "\n", - "fig = plt.figure(figsize=(10,20))\n", + "fig = plt.figure(figsize=(10, 20))\n", "col = 5\n", "row = math.ceil(len(dates) / col)\n", "for i in range(1, len(results_rgb) + 1):\n", " fig.add_subplot(row, col, i)\n", - " plt.imshow(reshape_as_image(results_rgb[i-1][1]))" + " plt.imshow(reshape_as_image(results_rgb[i - 1][1]))" ] }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "93982f5e7e9a4a01838b940b72b43b8c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - " 0%| | 0/85 [00:00" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "## Fetch NDVI\n", "\n", @@ -1240,49 +638,26 @@ ")\n", "\n", "with futures.ThreadPoolExecutor(max_workers=10) as executor:\n", - " future_work = [\n", - " executor.submit(bbox_worker, scene) for scene in sceneid\n", - " ]\n", + " future_work = [executor.submit(bbox_worker, scene) for scene in sceneid]\n", "\n", - " for f in tqdm(futures.as_completed(future_work), total=len(future_work)): \n", + " for f in tqdm(futures.as_completed(future_work), total=len(future_work)):\n", " pass\n", "\n", - "results_ndvi = list(_filter_futures(future_work))\n", + "results_ndvi = list(_filter_futures(future_work))\n", "\n", - "fig = plt.figure(figsize=(10,20))\n", + "fig = plt.figure(figsize=(10, 20))\n", "col = 5\n", "row = math.ceil(len(dates) / col)\n", - "for i in range(1, len(results_rgb) + 1):\n", + "for i in range(1, len(results_ndvi) + 1):\n", " fig.add_subplot(row, col, i)\n", - " plt.imshow(results_ndvi[i-1][1][0])" + " plt.imshow(results_ndvi[i - 1][1][0])" ] }, { "cell_type": "code", - "execution_count": 36, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 36, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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flj8bp5++mn9n3v27Il//vOfyM/Px7Zt27B7927k5OQgJycHKSkp2L17t/59OSIianls4IiIiJrQtWvXUFBQAAAYNmyYvn3Tpk0AgA8++AA//OEPfW5z+vTpR5orJiYGpaWleO+99xAREfHAt3M4HEhPT0d6ejoA4Pjx4xg/fjwOHDiAlStX4vXXX3+kPERE1Ph4ERMiIqImNHv2bJSXl+Opp55CSkqKvt17QZOYmJg6t8nJyal3LO8nYR6Pp976iBEjAHzXHD6qbt264Sc/+QkA4NixYw0ai4iIGhcbOCIioiZw+vRpjB07FqtWrYLL5cKqVat86t/73vcAACtWrPA5VXLfvn1499136x0zKioKAHDy5Ml667Nnz4bT6cScOXOwcePGOvXKykps2LAB58+fBwBUVFRg2bJluHr1qs9+mqYhPz8fABAbG/sAR0tERM2FP+RNRET0iLxXlfR+50zTNFy/fh3FxcX4+uuvISLo3Lkz1q5dW+cy/cXFxejTpw/Ky8vRtWtX9OzZE2VlZSgsLMTs2bPx+9//HnFxcfqFSQDg1q1biIuLw3/+8x8888wzSEhIgMViQWZmJgYMGACg5qcHxo8fj4qKCiQmJqJLly5wuVwoKyvDV199hfLychw5cgRJSUm4evUqwsPDYbPZ0LdvX8THx6Oqqgp///vf4Xa7ER8fj8OHDz/U6ZhERNS02MARERE9ojt/FsBqtSIkJARRUVHo27cv0tLSkJqaetcrOX799dd44403cOjQIdy8eRNPPvkkpk2bhtdeew1KqToNHAAcPnwY8+bNQ1FREa5fvw4RwYcffohJkybp+5SWlmLx4sXYuXMn3G43bDYboqKi0Lt3b2RkZCAtLQ2BgYHweDxYsWIFdu3ahaNHj+LixYsIDAxEhw4dMGrUKEyfPt3nyplERNTy2MARERERERGZBL8DR0REREREZBJs4IiIiIiIiEyCDRwREREREZFJsIEjIiIiIiIyCTZwREREREREJsEGjoiIiIiIyCTYwBEREREREZkEGzgiIiIiIiKTYANHRERERERkEmzgiIiIiIiITIINHBERERERkUmwgSMiIiIiIjIJNnBEREREREQmwQaOiIiIiIjIJNjAERERERERmQQbOCIiIiIiIpNgA0dERERERGQS/w/ouQYkb72MZQAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "stats = [_stats(data, mask) for _, data, mask in results_ndvi]\n", "\n", @@ -1308,7 +683,7 @@ ], "metadata": { "kernelspec": { - "display_name": "py39", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -1322,7 +697,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13 (main, May 24 2022, 21:13:51) \n[Clang 13.1.6 (clang-1316.0.21.2)]" + "version": "3.9.17" }, "vscode": { "interpreter": { diff --git a/docs/src/examples/notebooks/Working_with_STAC_simple.ipynb b/docs/src/examples/notebooks/Working_with_STAC_simple.ipynb index e52aeb858..ffc594007 100644 --- a/docs/src/examples/notebooks/Working_with_STAC_simple.ipynb +++ b/docs/src/examples/notebooks/Working_with_STAC_simple.ipynb @@ -47,20 +47,11 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "%pylab is deprecated, use %matplotlib inline and import the required libraries.\n", - "Populating the interactive namespace from numpy and matplotlib\n" - ] - } - ], + "outputs": [], "source": [ "import httpx\n", "\n", @@ -73,30 +64,24 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "titiler_endpoint = \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + "titiler_endpoint = (\n", + " \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + ")\n", "stac_item = \"https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a/items/S2A_30TVT_20221112_0_L2A\"" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": { "scrolled": true, "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'type': 'Feature', 'stac_version': '1.0.0', 'id': 'S2A_30TVT_20221112_0_L2A', 'properties': {'created': '2022-11-14T06:54:49.284Z', 'platform': 'sentinel-2a', 'constellation': 'sentinel-2', 'instruments': ['msi'], 'eo:cloud_cover': 0.005979, 'proj:epsg': 32630, 'mgrs:utm_zone': 30, 'mgrs:latitude_band': 'T', 'mgrs:grid_square': 'VT', 'grid:code': 'MGRS-30TVT', 'view:sun_azimuth': 169.467826196677, 'view:sun_elevation': 24.259740600657594, 's2:degraded_msi_data_percentage': 0, 's2:nodata_pixel_percentage': 0.000226, 's2:saturated_defective_pixel_percentage': 0, 's2:dark_features_percentage': 0, 's2:cloud_shadow_percentage': 0.002296, 's2:vegetation_percentage': 10.348745, 's2:not_vegetated_percentage': 2.478484, 's2:water_percentage': 87.111628, 's2:unclassified_percentage': 0.002548, 's2:medium_proba_clouds_percentage': 0.003716, 's2:high_proba_clouds_percentage': 0.000508, 's2:thin_cirrus_percentage': 0.001755, 's2:snow_ice_percentage': 0.050325, 's2:product_type': 'S2MSI2A', 's2:processing_baseline': '04.00', 's2:product_uri': 'S2A_MSIL2A_20221112T111321_N0400_R137_T30TVT_20221112T145700.SAFE', 's2:generation_time': '2022-11-12T14:57:00.000000Z', 's2:datatake_id': 'GS2A_20221112T111321_038601_N04.00', 's2:datatake_type': 'INS-NOBS', 's2:datastrip_id': 'S2A_OPER_MSI_L2A_DS_ATOS_20221112T145700_S20221112T111315_N04.00', 's2:granule_id': 'S2A_OPER_MSI_L2A_TL_ATOS_20221112T145700_A038601_T30TVT_N04.00', 's2:reflectance_conversion_factor': 1.0193600036007, 'datetime': '2022-11-12T11:18:11.455000Z', 's2:sequence': '0', 'earthsearch:s3_path': 's3://sentinel-cogs/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A', 'earthsearch:payload_id': 'roda-sentinel2/workflow-sentinel2-to-stac/d5f624f4b32b7ca4b39180d6eceea7fd', 'earthsearch:boa_offset_applied': True, 'processing:software': {'sentinel2-to-stac': '0.1.0'}, 'updated': '2022-11-14T06:54:49.284Z'}, 'geometry': {'type': 'Polygon', 'coordinates': [[[-4.337121116089946, 47.8459059875105], [-2.86954302848021, 47.85361872923358], [-2.8719559380291044, 46.865637260938634], [-4.312398603410253, 46.85818510451771], [-4.337121116089946, 47.8459059875105]]]}, 'links': [{'rel': 'self', 'href': 'https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a/items/S2A_30TVT_20221112_0_L2A'}, {'rel': 'canonical', 'href': 's3://sentinel-cogs/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/S2A_30TVT_20221112_0_L2A.json', 'type': 'application/json'}, {'rel': 'license', 'href': 'https://sentinel.esa.int/documents/247904/690755/Sentinel_Data_Legal_Notice'}, {'rel': 'derived_from', 'href': 'https://earth-search.aws.element84.com/v1/collections/sentinel-2-l1c/items/S2A_30TVT_20221112_0_L1C', 'type': 'application/geo+json'}, {'rel': 'parent', 'href': 'https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a'}, {'rel': 'collection', 'href': 'https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a'}, {'rel': 'root', 'href': 'https://earth-search.aws.element84.com/v1/'}], 'assets': {'aot': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/AOT.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Aerosol optical thickness (AOT)', 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.001, 'offset': 0}], 'roles': ['data', 'reflectance']}, 'blue': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B02.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Blue (band 2) - 10m', 'eo:bands': [{'name': 'blue', 'common_name': 'blue', 'description': 'Blue (band 2)', 'center_wavelength': 0.49, 'full_width_half_max': 0.098}], 'gsd': 10, 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 10, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'coastal': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B01.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Coastal aerosol (band 1) - 60m', 'eo:bands': [{'name': 'coastal', 'common_name': 'coastal', 'description': 'Coastal aerosol (band 1)', 'center_wavelength': 0.443, 'full_width_half_max': 0.027}], 'gsd': 60, 'proj:shape': [1830, 1830], 'proj:transform': [60, 0, 399960, 0, -60, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 60, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'granule_metadata': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/granule_metadata.xml', 'type': 'application/xml', 'roles': ['metadata']}, 'green': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B03.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Green (band 3) - 10m', 'eo:bands': [{'name': 'green', 'common_name': 'green', 'description': 'Green (band 3)', 'center_wavelength': 0.56, 'full_width_half_max': 0.045}], 'gsd': 10, 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 10, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'nir': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B08.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'NIR 1 (band 8) - 10m', 'eo:bands': [{'name': 'nir', 'common_name': 'nir', 'description': 'NIR 1 (band 8)', 'center_wavelength': 0.842, 'full_width_half_max': 0.145}], 'gsd': 10, 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 10, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'nir08': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B8A.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'NIR 2 (band 8A) - 20m', 'eo:bands': [{'name': 'nir08', 'common_name': 'nir08', 'description': 'NIR 2 (band 8A)', 'center_wavelength': 0.865, 'full_width_half_max': 0.033}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'nir09': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B09.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'NIR 3 (band 9) - 60m', 'eo:bands': [{'name': 'nir09', 'common_name': 'nir09', 'description': 'NIR 3 (band 9)', 'center_wavelength': 0.945, 'full_width_half_max': 0.026}], 'gsd': 60, 'proj:shape': [1830, 1830], 'proj:transform': [60, 0, 399960, 0, -60, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 60, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'red': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B04.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Red (band 4) - 10m', 'eo:bands': [{'name': 'red', 'common_name': 'red', 'description': 'Red (band 4)', 'center_wavelength': 0.665, 'full_width_half_max': 0.038}], 'gsd': 10, 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 10, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'rededge1': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B05.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Red edge 1 (band 5) - 20m', 'eo:bands': [{'name': 'rededge1', 'common_name': 'rededge', 'description': 'Red edge 1 (band 5)', 'center_wavelength': 0.704, 'full_width_half_max': 0.019}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'rededge2': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B06.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Red edge 2 (band 6) - 20m', 'eo:bands': [{'name': 'rededge2', 'common_name': 'rededge', 'description': 'Red edge 2 (band 6)', 'center_wavelength': 0.74, 'full_width_half_max': 0.018}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'rededge3': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B07.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Red edge 3 (band 7) - 20m', 'eo:bands': [{'name': 'rededge3', 'common_name': 'rededge', 'description': 'Red edge 3 (band 7)', 'center_wavelength': 0.783, 'full_width_half_max': 0.028}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'scl': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/SCL.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Scene classification map (SCL)', 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint8', 'spatial_resolution': 20}], 'roles': ['data', 'reflectance']}, 'swir16': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B11.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'SWIR 1 (band 11) - 20m', 'eo:bands': [{'name': 'swir16', 'common_name': 'swir16', 'description': 'SWIR 1 (band 11)', 'center_wavelength': 1.61, 'full_width_half_max': 0.143}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'swir22': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/B12.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'SWIR 2 (band 12) - 20m', 'eo:bands': [{'name': 'swir22', 'common_name': 'swir22', 'description': 'SWIR 2 (band 12)', 'center_wavelength': 2.19, 'full_width_half_max': 0.242}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'thumbnail': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/thumbnail.jpg', 'type': 'image/jpeg', 'title': 'Thumbnail image', 'roles': ['thumbnail']}, 'tileinfo_metadata': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/tileinfo_metadata.json', 'type': 'application/json', 'roles': ['metadata']}, 'visual': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/TCI.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'True color image', 'eo:bands': [{'name': 'red', 'common_name': 'red', 'description': 'Red (band 4)', 'center_wavelength': 0.665, 'full_width_half_max': 0.038}, {'name': 'green', 'common_name': 'green', 'description': 'Green (band 3)', 'center_wavelength': 0.56, 'full_width_half_max': 0.045}, {'name': 'blue', 'common_name': 'blue', 'description': 'Blue (band 2)', 'center_wavelength': 0.49, 'full_width_half_max': 0.098}], 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'roles': ['visual']}, 'wvp': {'href': 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/30/T/VT/2022/11/S2A_30TVT_20221112_0_L2A/WVP.tif', 'type': 'image/tiff; application=geotiff; profile=cloud-optimized', 'title': 'Water vapour (WVP)', 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'unit': 'cm', 'scale': 0.001, 'offset': 0}], 'roles': ['data', 'reflectance']}, 'aot-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/AOT.jp2', 'type': 'image/jp2', 'title': 'Aerosol optical thickness (AOT)', 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.001, 'offset': 0}], 'roles': ['data', 'reflectance']}, 'blue-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B02.jp2', 'type': 'image/jp2', 'title': 'Blue (band 2) - 10m', 'eo:bands': [{'name': 'blue', 'common_name': 'blue', 'description': 'Blue (band 2)', 'center_wavelength': 0.49, 'full_width_half_max': 0.098}], 'gsd': 10, 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 10, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'coastal-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B01.jp2', 'type': 'image/jp2', 'title': 'Coastal aerosol (band 1) - 60m', 'eo:bands': [{'name': 'coastal', 'common_name': 'coastal', 'description': 'Coastal aerosol (band 1)', 'center_wavelength': 0.443, 'full_width_half_max': 0.027}], 'gsd': 60, 'proj:shape': [1830, 1830], 'proj:transform': [60, 0, 399960, 0, -60, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 60, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'green-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B03.jp2', 'type': 'image/jp2', 'title': 'Green (band 3) - 10m', 'eo:bands': [{'name': 'green', 'common_name': 'green', 'description': 'Green (band 3)', 'center_wavelength': 0.56, 'full_width_half_max': 0.045}], 'gsd': 10, 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 10, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'nir-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B08.jp2', 'type': 'image/jp2', 'title': 'NIR 1 (band 8) - 10m', 'eo:bands': [{'name': 'nir', 'common_name': 'nir', 'description': 'NIR 1 (band 8)', 'center_wavelength': 0.842, 'full_width_half_max': 0.145}], 'gsd': 10, 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 10, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'nir08-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B8A.jp2', 'type': 'image/jp2', 'title': 'NIR 2 (band 8A) - 20m', 'eo:bands': [{'name': 'nir08', 'common_name': 'nir08', 'description': 'NIR 2 (band 8A)', 'center_wavelength': 0.865, 'full_width_half_max': 0.033}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'nir09-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B09.jp2', 'type': 'image/jp2', 'title': 'NIR 3 (band 9) - 60m', 'eo:bands': [{'name': 'nir09', 'common_name': 'nir09', 'description': 'NIR 3 (band 9)', 'center_wavelength': 0.945, 'full_width_half_max': 0.026}], 'gsd': 60, 'proj:shape': [1830, 1830], 'proj:transform': [60, 0, 399960, 0, -60, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 60, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'red-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B04.jp2', 'type': 'image/jp2', 'title': 'Red (band 4) - 10m', 'eo:bands': [{'name': 'red', 'common_name': 'red', 'description': 'Red (band 4)', 'center_wavelength': 0.665, 'full_width_half_max': 0.038}], 'gsd': 10, 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 10, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'rededge1-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B05.jp2', 'type': 'image/jp2', 'title': 'Red edge 1 (band 5) - 20m', 'eo:bands': [{'name': 'rededge1', 'common_name': 'rededge', 'description': 'Red edge 1 (band 5)', 'center_wavelength': 0.704, 'full_width_half_max': 0.019}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'rededge2-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B06.jp2', 'type': 'image/jp2', 'title': 'Red edge 2 (band 6) - 20m', 'eo:bands': [{'name': 'rededge2', 'common_name': 'rededge', 'description': 'Red edge 2 (band 6)', 'center_wavelength': 0.74, 'full_width_half_max': 0.018}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'rededge3-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B07.jp2', 'type': 'image/jp2', 'title': 'Red edge 3 (band 7) - 20m', 'eo:bands': [{'name': 'rededge3', 'common_name': 'rededge', 'description': 'Red edge 3 (band 7)', 'center_wavelength': 0.783, 'full_width_half_max': 0.028}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'scl-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/SCL.jp2', 'type': 'image/jp2', 'title': 'Scene classification map (SCL)', 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint8', 'spatial_resolution': 20}], 'roles': ['data', 'reflectance']}, 'swir16-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B11.jp2', 'type': 'image/jp2', 'title': 'SWIR 1 (band 11) - 20m', 'eo:bands': [{'name': 'swir16', 'common_name': 'swir16', 'description': 'SWIR 1 (band 11)', 'center_wavelength': 1.61, 'full_width_half_max': 0.143}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'swir22-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/B12.jp2', 'type': 'image/jp2', 'title': 'SWIR 2 (band 12) - 20m', 'eo:bands': [{'name': 'swir22', 'common_name': 'swir22', 'description': 'SWIR 2 (band 12)', 'center_wavelength': 2.19, 'full_width_half_max': 0.242}], 'gsd': 20, 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'scale': 0.0001, 'offset': -0.1}], 'roles': ['data', 'reflectance']}, 'visual-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/TCI.jp2', 'type': 'image/jp2', 'title': 'True color image', 'eo:bands': [{'name': 'red', 'common_name': 'red', 'description': 'Red (band 4)', 'center_wavelength': 0.665, 'full_width_half_max': 0.038}, {'name': 'green', 'common_name': 'green', 'description': 'Green (band 3)', 'center_wavelength': 0.56, 'full_width_half_max': 0.045}, {'name': 'blue', 'common_name': 'blue', 'description': 'Blue (band 2)', 'center_wavelength': 0.49, 'full_width_half_max': 0.098}], 'proj:shape': [10980, 10980], 'proj:transform': [10, 0, 399960, 0, -10, 5300040], 'roles': ['visual']}, 'wvp-jp2': {'href': 's3://sentinel-s2-l2a/tiles/30/T/VT/2022/11/12/0/WVP.jp2', 'type': 'image/jp2', 'title': 'Water vapour (WVP)', 'proj:shape': [5490, 5490], 'proj:transform': [20, 0, 399960, 0, -20, 5300040], 'raster:bands': [{'nodata': 0, 'data_type': 'uint16', 'bits_per_sample': 15, 'spatial_resolution': 20, 'unit': 'cm', 'scale': 0.001, 'offset': 0}], 'roles': ['data', 'reflectance']}}, 'bbox': [-4.337121116089946, 46.85818510451771, -2.86954302848021, 47.85361872923358], 'stac_extensions': ['https://stac-extensions.github.io/grid/v1.0.0/schema.json', 'https://stac-extensions.github.io/eo/v1.0.0/schema.json', 'https://stac-extensions.github.io/mgrs/v1.0.0/schema.json', 'https://stac-extensions.github.io/projection/v1.0.0/schema.json', 'https://stac-extensions.github.io/processing/v1.1.0/schema.json', 'https://stac-extensions.github.io/view/v1.0.0/schema.json', 'https://stac-extensions.github.io/raster/v1.1.0/schema.json'], 'collection': 'sentinel-2-l2a'}\n" - ] - } - ], + "outputs": [], "source": [ "item = httpx.get(stac_item).json()\n", "print(item)" @@ -104,53 +89,11 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Name: aot | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: blue | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: coastal | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: granule_metadata | Format: application/xml\n", - "Name: green | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: nir | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: nir08 | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: nir09 | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: red | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: rededge1 | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: rededge2 | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: rededge3 | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: scl | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: swir16 | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: swir22 | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: thumbnail | Format: image/jpeg\n", - "Name: tileinfo_metadata | Format: application/json\n", - "Name: visual | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: wvp | Format: image/tiff; application=geotiff; profile=cloud-optimized\n", - "Name: aot-jp2 | Format: image/jp2\n", - "Name: blue-jp2 | Format: image/jp2\n", - "Name: coastal-jp2 | Format: image/jp2\n", - "Name: green-jp2 | Format: image/jp2\n", - "Name: nir-jp2 | Format: image/jp2\n", - "Name: nir08-jp2 | Format: image/jp2\n", - "Name: nir09-jp2 | Format: image/jp2\n", - "Name: red-jp2 | Format: image/jp2\n", - "Name: rededge1-jp2 | Format: image/jp2\n", - "Name: rededge2-jp2 | Format: image/jp2\n", - "Name: rededge3-jp2 | Format: image/jp2\n", - "Name: scl-jp2 | Format: image/jp2\n", - "Name: swir16-jp2 | Format: image/jp2\n", - "Name: swir22-jp2 | Format: image/jp2\n", - "Name: visual-jp2 | Format: image/jp2\n", - "Name: wvp-jp2 | Format: image/jp2\n" - ] - } - ], + "outputs": [], "source": [ "for it, asset in item[\"assets\"].items():\n", " print(\"Name:\", it, \"| Format:\", asset[\"type\"])" @@ -158,111 +101,16 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "bounds = featureBounds(item)\n", "\n", "m = Map(\n", " tiles=\"OpenStreetMap\",\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=8\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2),\n", + " zoom_start=8,\n", ")\n", "\n", "geo_json = GeoJson(data=item)\n", @@ -272,28 +120,23 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'visual': {'bounds': [-4.337134709547373, 46.85817595750231, -2.869529638083867, 47.85370180403547], 'minzoom': 7, 'maxzoom': 13, 'band_metadata': [['b1', {}], ['b2', {}], ['b3', {}]], 'band_descriptions': [['b1', ''], ['b2', ''], ['b3', '']], 'dtype': 'uint8', 'nodata_type': 'Nodata', 'colorinterp': ['red', 'green', 'blue'], 'height': 10980, 'driver': 'GTiff', 'overviews': [2, 4, 8, 16], 'count': 3, 'width': 10980, 'nodata_value': 0.0}, 'red': {'bounds': [-4.337134709547373, 46.85817595750231, -2.869529638083867, 47.85370180403547], 'minzoom': 7, 'maxzoom': 13, 'band_metadata': [['b1', {}]], 'band_descriptions': [['b1', '']], 'dtype': 'uint16', 'nodata_type': 'Nodata', 'colorinterp': ['gray'], 'height': 10980, 'driver': 'GTiff', 'overviews': [2, 4, 8, 16], 'count': 1, 'width': 10980, 'nodata_value': 0.0}, 'blue': {'bounds': [-4.337134709547373, 46.85817595750231, -2.869529638083867, 47.85370180403547], 'minzoom': 7, 'maxzoom': 13, 'band_metadata': [['b1', {}]], 'band_descriptions': [['b1', '']], 'dtype': 'uint16', 'nodata_type': 'Nodata', 'colorinterp': ['gray'], 'height': 10980, 'driver': 'GTiff', 'overviews': [2, 4, 8, 16], 'count': 1, 'width': 10980, 'nodata_value': 0.0}, 'green': {'bounds': [-4.337134709547373, 46.85817595750231, -2.869529638083867, 47.85370180403547], 'minzoom': 7, 'maxzoom': 13, 'band_metadata': [['b1', {}]], 'band_descriptions': [['b1', '']], 'dtype': 'uint16', 'nodata_type': 'Nodata', 'colorinterp': ['gray'], 'height': 10980, 'driver': 'GTiff', 'overviews': [2, 4, 8, 16], 'count': 1, 'width': 10980, 'nodata_value': 0.0}}\n" - ] - } - ], + "outputs": [], "source": [ "# Get Tile URL\n", "r = httpx.get(\n", " f\"{titiler_endpoint}/stac/info\",\n", - " params = (\n", + " params=(\n", " (\"url\", stac_item),\n", " # Get info for multiple assets\n", - " (\"assets\",\"visual\"), (\"assets\",\"red\"), (\"assets\",\"blue\"), (\"assets\",\"green\"),\n", - " )\n", + " (\"assets\", \"visual\"),\n", + " (\"assets\", \"red\"),\n", + " (\"assets\", \"blue\"),\n", + " (\"assets\", \"green\"),\n", + " ),\n", ").json()\n", "print(r)" ] @@ -307,105 +150,22 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/stac/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/stac/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": stac_item,\n", " \"assets\": \"visual\",\n", " \"minzoom\": 8, # By default titiler will use 0\n", - " \"maxzoom\": 14, # By default titiler will use 24\n", - " }\n", + " \"maxzoom\": 14, # By default titiler will use 24\n", + " },\n", ").json()\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=10\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=10\n", ")\n", "\n", "tiles = TileLayer(\n", @@ -413,7 +173,7 @@ " min_zoom=r[\"minzoom\"],\n", " max_zoom=r[\"maxzoom\"],\n", " opacity=1,\n", - " attr=\"ESA\"\n", + " attr=\"ESA\",\n", ")\n", "tiles.add_to(m)\n", "m" @@ -428,107 +188,24 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Get Tile URL\n", "r = httpx.get(\n", - " f\"{titiler_endpoint}/stac/tilejson.json\",\n", - " params = {\n", + " f\"{titiler_endpoint}/stac/WebMercatorQuad/tilejson.json\",\n", + " params={\n", " \"url\": stac_item,\n", " \"assets\": \"visual\",\n", " \"asset_bidx\": \"visual|3,1,2\",\n", " \"minzoom\": 8, # By default titiler will use 0\n", - " \"maxzoom\": 14, # By default titiler will use 24\n", - " }\n", + " \"maxzoom\": 14, # By default titiler will use 24\n", + " },\n", ").json()\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=12\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=12\n", ")\n", "\n", "tiles = TileLayer(\n", @@ -536,7 +213,7 @@ " min_zoom=r[\"minzoom\"],\n", " max_zoom=r[\"maxzoom\"],\n", " opacity=1,\n", - " attr=\"ESA\"\n", + " attr=\"ESA\",\n", ")\n", "tiles.add_to(m)\n", "m" @@ -544,96 +221,14 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Get Tile URL\n", "r = httpx.get(\n", - " f\"{titiler_endpoint}/stac/tilejson.json\",\n", - " params = (\n", + " f\"{titiler_endpoint}/stac/WebMercatorQuad/tilejson.json\",\n", + " params=(\n", " (\"url\", stac_item),\n", " (\"assets\", \"red\"),\n", " (\"assets\", \"green\"),\n", @@ -646,12 +241,11 @@ " (\"minzoom\", 8),\n", " (\"maxzoom\", 14),\n", " (\"rescale\", \"0,2000\"),\n", - " )\n", + " ),\n", ").json()\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=11\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=11\n", ")\n", "\n", "tiles = TileLayer(\n", @@ -659,7 +253,7 @@ " min_zoom=r[\"minzoom\"],\n", " max_zoom=r[\"maxzoom\"],\n", " opacity=1,\n", - " attr=\"ESA\"\n", + " attr=\"ESA\",\n", ")\n", "tiles.add_to(m)\n", "m" @@ -669,116 +263,33 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Apply Expression between assets" + "Use an expression to calculate a band index (NDVI) based on information contained in multiple assets." ] }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Get Tile URL\n", "r = httpx.get(\n", - " f\"{titiler_endpoint}/stac/tilejson.json\",\n", - " params = (\n", + " f\"{titiler_endpoint}/stac/WebMercatorQuad/tilejson.json\",\n", + " params=(\n", " (\"url\", stac_item),\n", " (\"expression\", \"(nir-red)/(nir+red)\"), # NDVI\n", - " # We need to tell rio-tiler that each asset is a Band \n", + " # We need to tell rio-tiler that each asset is a Band\n", " # (so it will select the first band within each asset automatically)\n", " (\"asset_as_band\", True),\n", " (\"rescale\", \"-1,1\"),\n", " (\"minzoom\", 8),\n", " (\"maxzoom\", 14),\n", " (\"colormap_name\", \"viridis\"),\n", - " )\n", + " ),\n", ").json()\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=10\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=10\n", ")\n", "\n", "tiles = TileLayer(\n", @@ -786,117 +297,40 @@ " min_zoom=r[\"minzoom\"],\n", " max_zoom=r[\"maxzoom\"],\n", " opacity=1,\n", - " attr=\"ESA\"\n", + " attr=\"ESA\",\n", ")\n", "tiles.add_to(m)\n", "m" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you don't use the `asset_as_band=True` option, you need to append the band to the asset name within the expression. For example, `nir` becomes `nir_b1`." + ] + }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
Make this Notebook Trusted to load map: File -> Trust Notebook
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Get Tile URL\n", "r = httpx.get(\n", - " f\"{titiler_endpoint}/stac/tilejson.json\",\n", - " params = (\n", + " f\"{titiler_endpoint}/stac/WebMercatorQuad/tilejson.json\",\n", + " params=(\n", " (\"url\", stac_item),\n", - " # if you don't use `asset_as_band=True` option you need to pass the band indexes within the expression\n", " (\"expression\", \"(nir_b1-red_b1)/(nir_b1+red_b1)\"), # NDVI\n", " (\"rescale\", \"-1,1\"),\n", " (\"minzoom\", 8),\n", " (\"maxzoom\", 14),\n", " (\"colormap_name\", \"viridis\"),\n", - " )\n", + " ),\n", ").json()\n", "\n", "m = Map(\n", - " location=((bounds[1] + bounds[3]) / 2,(bounds[0] + bounds[2]) / 2),\n", - " zoom_start=10\n", + " location=((bounds[1] + bounds[3]) / 2, (bounds[0] + bounds[2]) / 2), zoom_start=10\n", ")\n", "\n", "tiles = TileLayer(\n", @@ -904,7 +338,7 @@ " min_zoom=r[\"minzoom\"],\n", " max_zoom=r[\"maxzoom\"],\n", " opacity=1,\n", - " attr=\"ESA\"\n", + " attr=\"ESA\",\n", ")\n", "tiles.add_to(m)\n", "m" diff --git a/docs/src/examples/notebooks/Working_with_Statistics.ipynb b/docs/src/examples/notebooks/Working_with_Statistics.ipynb index d9f2ac5e4..83adc19c0 100644 --- a/docs/src/examples/notebooks/Working_with_Statistics.ipynb +++ b/docs/src/examples/notebooks/Working_with_Statistics.ipynb @@ -2,139 +2,95 @@ "cells": [ { "cell_type": "markdown", - "source": [ - "# Working with Statistics" - ], "metadata": { "collapsed": false - } + }, + "source": [ + "# Working with Statistics" + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "## Intro\n", "\n", "Titiler allows you to get statistics and summaries of your data without having to load the entire dataset yourself. These statistics can be summaries of entire COG files, STAC items, or individual parts of the file, specified using GeoJSON.\n", "\n", - "Below, we will go over some of the statistical endpoints in Titiler - `/bounds`, `/info`, and `/statistics`.\n", + "Below, we will go over some of the statistical endpoints in Titiler - `/info` and `/statistics`.\n", "\n", "(Note: these examples will be using the `/cog` endpoint, but everything is also available for `/stac` and `/mosaicjson` unless otherwise noted)" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 16, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:40.161502Z", + "start_time": "2023-04-06T14:25:40.153667Z" + }, + "collapsed": false + }, "outputs": [], "source": [ "# setup\n", "import httpx\n", "import json\n", "\n", - "titiler_endpoint = \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + "titiler_endpoint = (\n", + " \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + ")\n", "cog_url = \"https://opendata.digitalglobe.com/events/mauritius-oil-spill/post-event/2020-08-12/105001001F1B5B00/105001001F1B5B00.tif\"" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:40.153667Z", - "end_time": "2023-04-06T14:25:40.161502Z" - } - } + ] }, { "cell_type": "markdown", - "source": [ - "## Bounds\n", - "\n", - "The `/bounds` endpoint returns the bounding box of the image/asset. These bounds are returned in the projection EPSG:4326 (WGS84), in the format `(minx, miny, maxx, maxy)`." - ], "metadata": { "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": 10, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'bounds': [57.664053823239804, -20.55473177712791, 57.84021477996238, -20.25261582755764]}\n" - ] - } - ], + }, "source": [ - "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/bounds\",\n", - " params = {\n", - " \"url\": cog_url,\n", - " }\n", - ").json()\n", + "## Info\n", "\n", - "bounds = r[\"bounds\"]\n", - "print(r)" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:40.781598Z", - "end_time": "2023-04-06T14:25:40.921234Z" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "For a bit more information, you can get summary statistics from the `/info` endpoint. This includes info such as:\n", - "- Bounds (identical to the `/bounds` endpoint)\n", - "- Min and max zoom\n", + "The `/info` endpoint returns general metadata about the image/asset.\n", + "\n", + "- Bounds\n", + "- CRS \n", "- Band metadata, such as names of the bands and their descriptions\n", "- Number of bands in the image\n", "- Overview levels\n", - "- Image width and height\n", - "\n", - "These are statistics available in the metadata of the image, so should be fast to read.\n" - ], - "metadata": { - "collapsed": false - } + "- Image width and height" + ] }, { "cell_type": "code", - "execution_count": 11, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\"bounds\": [57.664053823239804, -20.55473177712791, 57.84021477996238, -20.25261582755764], \"minzoom\": 10, \"maxzoom\": 18, \"band_metadata\": [[\"b1\", {}], [\"b2\", {}], [\"b3\", {}]], \"band_descriptions\": [[\"b1\", \"\"], [\"b2\", \"\"], [\"b3\", \"\"]], \"dtype\": \"uint8\", \"nodata_type\": \"Mask\", \"colorinterp\": [\"red\", \"green\", \"blue\"], \"count\": 3, \"width\": 38628, \"driver\": \"GTiff\", \"overviews\": [2, 4, 8, 16, 32, 64, 128], \"height\": 66247}\n" - ] - } - ], + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:42.410135Z", + "start_time": "2023-04-06T14:25:42.355858Z" + }, + "collapsed": false + }, + "outputs": [], "source": [ "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/info\",\n", - " params = {\n", + " params={\n", " \"url\": cog_url,\n", - " }\n", + " },\n", ").json()\n", "\n", "print(json.dumps(r))" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:42.355858Z", - "end_time": "2023-04-06T14:25:42.410135Z" - } - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "## Statistics\n", "\n", @@ -144,98 +100,86 @@ "- Percentiles\n", "\n", "Statistics are generated both for the image as a whole and for each band individually." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", - "execution_count": 12, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\"b1\": {\"min\": 0.0, \"max\": 255.0, \"mean\": 36.94901407469342, \"count\": 574080.0, \"sum\": 21211690.0, \"std\": 48.282133573955264, \"median\": 3.0, \"majority\": 1.0, \"minority\": 246.0, \"unique\": 256.0, \"histogram\": [[330584.0, 54820.0, 67683.0, 57434.0, 30305.0, 14648.0, 9606.0, 5653.0, 2296.0, 1051.0], [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0]], \"valid_percent\": 93.75, \"masked_pixels\": 38272.0, \"valid_pixels\": 574080.0, \"percentile_2\": 0.0, \"percentile_98\": 171.0}, \"b2\": {\"min\": 0.0, \"max\": 255.0, \"mean\": 57.1494356187291, \"count\": 574080.0, \"sum\": 32808348.0, \"std\": 56.300819175100656, \"median\": 37.0, \"majority\": 5.0, \"minority\": 0.0, \"unique\": 256.0, \"histogram\": [[271018.0, 34938.0, 54030.0, 69429.0, 70260.0, 32107.0, 29375.0, 9697.0, 2001.0, 1225.0], [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0]], \"valid_percent\": 93.75, \"masked_pixels\": 38272.0, \"valid_pixels\": 574080.0, \"percentile_2\": 5.0, \"percentile_98\": 180.0}, \"b3\": {\"min\": 0.0, \"max\": 255.0, \"mean\": 51.251764562430324, \"count\": 574080.0, \"sum\": 29422613.0, \"std\": 39.65505035854822, \"median\": 36.0, \"majority\": 16.0, \"minority\": 252.0, \"unique\": 254.0, \"histogram\": [[203263.0, 150865.0, 104882.0, 42645.0, 30652.0, 25382.0, 12434.0, 2397.0, 1097.0, 463.0], [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0]], \"valid_percent\": 93.75, \"masked_pixels\": 38272.0, \"valid_pixels\": 574080.0, \"percentile_2\": 14.0, \"percentile_98\": 158.0}}\n" - ] - } - ], + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:42.866905Z", + "start_time": "2023-04-06T14:25:42.816337Z" + }, + "collapsed": false + }, + "outputs": [], "source": [ "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/statistics\",\n", - " params = {\n", + " params={\n", " \"url\": cog_url,\n", - " }\n", + " },\n", ").json()\n", "\n", "print(json.dumps(r))" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:42.816337Z", - "end_time": "2023-04-06T14:25:42.866905Z" - } - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ - "This endpoint is far more configurable than `/bounds` and `info`. You can specify which bands to analyse, how to generate the histogram, and pre-process the image.\n", + "This endpoint is far more configurable than `/info`. You can specify which bands to analyse, how to generate the histogram, and pre-process the image.\n", "\n", "For example, if you wanted to get the statistics of the [VARI](https://www.space4water.org/space/visible-atmospherically-resistant-index-vari) of the image you can use the `expression` parameter to conduct simple band math:" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", - "execution_count": 13, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\"(b2-b1)/(b1+b2-b3)\": {\"min\": -1.7976931348623157e+308, \"max\": 1.7976931348623157e+308, \"mean\": null, \"count\": 574080.0, \"sum\": null, \"std\": null, \"median\": -0.15384615384615385, \"majority\": -0.4, \"minority\": -149.0, \"unique\": 18718.0, \"histogram\": [[5646.0, 10176.0, 130905.0, 97746.0, 50184.0, 95842.0, 60322.0, 21478.0, 13552.0, 12204.0], [-1.0, -0.8, -0.6, -0.3999999999999999, -0.19999999999999996, 0.0, 0.20000000000000018, 0.40000000000000013, 0.6000000000000001, 0.8, 1.0]], \"valid_percent\": 93.75, \"masked_pixels\": 38272.0, \"valid_pixels\": 574080.0, \"percentile_2\": -3.5, \"percentile_98\": 3.3870967741935485}}\n" - ] - } - ], + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:43.393610Z", + "start_time": "2023-04-06T14:25:43.304442Z" + }, + "collapsed": false + }, + "outputs": [], "source": [ "r = httpx.get(\n", " f\"{titiler_endpoint}/cog/statistics\",\n", - " params = {\n", + " params={\n", " \"url\": cog_url,\n", - " \"expression\": \"(b2-b1)/(b1+b2-b3)\", # expression for the VARI\n", - " \"histogram_range\": \"-1,1\"\n", - " }\n", + " \"expression\": \"(b2-b1)/(b1+b2-b3)\", # expression for the VARI\n", + " \"histogram_range\": \"-1,1\",\n", + " },\n", ").json()\n", "\n", "print(json.dumps(r))" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:43.304442Z", - "end_time": "2023-04-06T14:25:43.393610Z" - } - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false + }, "source": [ "Alternatively, if you would like to get statistics for only a certain area, you can specify an area via a feature or a feature collection.\n", "\n", "(Note: this endpoint is not available in the mosaicjson endpoint, only `/cog` and `/stac`)" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 5, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:43.877434Z", + "start_time": "2023-04-06T14:25:43.867923Z" + }, + "collapsed": false + }, "outputs": [], "source": [ "mahebourg = \"\"\"\n", @@ -284,55 +228,40 @@ " ]\n", "}\n", "\"\"\"" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:43.867923Z", - "end_time": "2023-04-06T14:25:43.877434Z" - } - } + ] }, { "cell_type": "code", - "execution_count": 15, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{\"type\": \"FeatureCollection\", \"features\": [{\"type\": \"Feature\", \"geometry\": {\"type\": \"Polygon\", \"coordinates\": [[[57.70358910197049, -20.384114558699935], [57.68564920588395, -20.384114558699935], [57.68209507552771, -20.39855066753664], [57.68666467170024, -20.421074640746554], [57.70341985766697, -20.434397129770545], [57.72999121319131, -20.42392955694521], [57.70358910197049, -20.384114558699935]]]}, \"properties\": {\"statistics\": {\"b1\": {\"min\": 0.0, \"max\": 255.0, \"mean\": 88.5634794986129, \"count\": 619641.0, \"sum\": 54877563.0, \"std\": 55.18714964714274, \"median\": 77.0, \"majority\": 52.0, \"minority\": 253.0, \"unique\": 256.0, \"histogram\": [[67233.0, 110049.0, 129122.0, 90849.0, 77108.0, 44091.0, 44606.0, 37790.0, 18033.0, 760.0], [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0]], \"valid_percent\": 62.0, \"masked_pixels\": 379783.0, \"valid_pixels\": 619641.0, \"percentile_2\": 4.0, \"percentile_98\": 208.0}, \"b2\": {\"min\": 0.0, \"max\": 255.0, \"mean\": 112.07155594933195, \"count\": 619641.0, \"sum\": 69444131.0, \"std\": 42.64508357271268, \"median\": 107.0, \"majority\": 103.0, \"minority\": 1.0, \"unique\": 256.0, \"histogram\": [[6004.0, 31108.0, 107187.0, 126848.0, 130731.0, 73650.0, 107827.0, 33264.0, 2403.0, 619.0], [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0]], \"valid_percent\": 62.0, \"masked_pixels\": 379783.0, \"valid_pixels\": 619641.0, \"percentile_2\": 34.0, \"percentile_98\": 189.0}, \"b3\": {\"min\": 0.0, \"max\": 255.0, \"mean\": 84.54690377170006, \"count\": 619641.0, \"sum\": 52388728.0, \"std\": 44.64862735915829, \"median\": 77.0, \"majority\": 53.0, \"minority\": 254.0, \"unique\": 256.0, \"histogram\": [[40704.0, 130299.0, 138014.0, 85866.0, 86381.0, 91182.0, 41872.0, 4116.0, 993.0, 214.0], [0.0, 25.5, 51.0, 76.5, 102.0, 127.5, 153.0, 178.5, 204.0, 229.5, 255.0]], \"valid_percent\": 62.0, \"masked_pixels\": 379783.0, \"valid_pixels\": 619641.0, \"percentile_2\": 11.0, \"percentile_98\": 170.0}}}}]}\n" - ] - } - ], + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2023-04-06T14:25:45.709013Z", + "start_time": "2023-04-06T14:25:44.592051Z" + }, + "collapsed": false + }, + "outputs": [], "source": [ "# NOTE: This is a POST request, unlike all other requests in this example\n", "r = httpx.post(\n", " f\"{titiler_endpoint}/cog/statistics\",\n", " data=mahebourg,\n", - " params = {\n", + " params={\n", " \"url\": cog_url,\n", - " }\n", + " \"max_size\": 1024,\n", + " },\n", + " timeout=20,\n", ").json()\n", "\n", - "print(json.dumps(r))\n" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "start_time": "2023-04-06T14:25:44.592051Z", - "end_time": "2023-04-06T14:25:45.709013Z" - } - } + "print(json.dumps(r))" + ] }, { "cell_type": "code", "execution_count": null, + "metadata": {}, "outputs": [], - "source": [], - "metadata": { - "collapsed": false - } + "source": [] } ], "metadata": { @@ -344,14 +273,14 @@ "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" + "pygments_lexer": "ipython3", + "version": "3.9.19" } }, "nbformat": 4, diff --git a/docs/src/examples/notebooks/Working_with_nonWebMercatorTMS.ipynb b/docs/src/examples/notebooks/Working_with_nonWebMercatorTMS.ipynb index c9e728f6b..e2744b8c3 100644 --- a/docs/src/examples/notebooks/Working_with_nonWebMercatorTMS.ipynb +++ b/docs/src/examples/notebooks/Working_with_nonWebMercatorTMS.ipynb @@ -36,7 +36,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -51,18 +51,20 @@ " TileLayer,\n", " WMSLayer,\n", " GeoJSON,\n", - " projections\n", + " projections,\n", ")" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "titiler_endpoint = \"http://127.0.0.1:8081\" # \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", - "url = \"https://s3.amazonaws.com/opendata.remotepixel.ca/cogs/natural_earth/world.tif\" # Natural Earth WORLD tif" + "titiler_endpoint = (\n", + " \"https://titiler.xyz\" # Developmentseed Demo endpoint. Please be kind.\n", + ")\n", + "url = \"https://s3.amazonaws.com/opendata.remotepixel.ca/cogs/natural_earth/world.tif\" # Natural Earth WORLD tif" ] }, { @@ -74,31 +76,11 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": { "scrolled": false }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Supported TMS:\n", - "- LINZAntarticaMapTilegrid\n", - "- EuropeanETRS89_LAEAQuad\n", - "- CanadianNAD83_LCC\n", - "- UPSArcticWGS84Quad\n", - "- NZTM2000\n", - "- NZTM2000Quad\n", - "- UTM31WGS84Quad\n", - "- UPSAntarcticWGS84Quad\n", - "- WorldMercatorWGS84Quad\n", - "- WGS1984Quad\n", - "- WorldCRS84Quad\n", - "- WebMercatorQuad\n" - ] - } - ], + "outputs": [], "source": [ "r = httpx.get(f\"{titiler_endpoint}/tileMatrixSets\").json()\n", "\n", @@ -119,28 +101,12 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "7d78a0adf5954e65b3f46db3cf943f7a", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Map(center=[0, 0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoom_out_text'…" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/tilejson.json\", params = {\"url\": url}\n", + " f\"{titiler_endpoint}/cog/WebMercatorQuad/tilejson.json\", params={\"url\": url}\n", ").json()\n", "\n", "m = Map(center=(0, 0), zoom=2, basemap={}, crs=projections.EPSG3857)\n", @@ -161,28 +127,12 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "8c5d6ab05bce4aef9e29ff7ebd0ac02f", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Map(center=[0, 0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoom_out_text'…" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/WorldCRS84Quad/tilejson.json\", params = {\"url\": url}\n", + " f\"{titiler_endpoint}/cog/WorldCRS84Quad/tilejson.json\", params={\"url\": url}\n", ").json()\n", "\n", "m = Map(center=(0, 0), zoom=1, basemap={}, crs=projections.EPSG4326)\n", @@ -203,43 +153,20 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0615b0bb04ad46198d05f6eb95ed8e6b", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Map(center=[50, 65], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoom_out_tex…" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "r = httpx.get(\n", - " f\"{titiler_endpoint}/cog/EuropeanETRS89_LAEAQuad/tilejson.json\", params = {\"url\": url}\n", + " f\"{titiler_endpoint}/cog/EuropeanETRS89_LAEAQuad/tilejson.json\", params={\"url\": url}\n", ").json()\n", "\n", "my_projection = {\n", - " 'name': 'EPSG:3035',\n", - " 'custom': True, #This is important, it tells ipyleaflet that this projection is not on the predefined ones.\n", - " 'proj4def': '+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs',\n", - " 'origin': [6500000.0, 5500000.0],\n", - " 'resolutions': [\n", - " 8192.0,\n", - " 4096.0,\n", - " 2048.0,\n", - " 1024.0,\n", - " 512.0,\n", - " 256.0\n", - " ]\n", + " \"name\": \"EPSG:3035\",\n", + " \"custom\": True, # This is important, it tells ipyleaflet that this projection is not on the predefined ones.\n", + " \"proj4def\": \"+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs\",\n", + " \"origin\": [6500000.0, 5500000.0],\n", + " \"resolutions\": [8192.0, 4096.0, 2048.0, 1024.0, 512.0, 256.0],\n", "}\n", "\n", "m = Map(center=(50, 65), zoom=0, basemap={}, crs=my_projection)\n", @@ -273,7 +200,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.9.18" }, "vscode": { "interpreter": { diff --git a/docs/src/external_links.md b/docs/src/external_links.md index 9cd60516a..be8a6de95 100644 --- a/docs/src/external_links.md +++ b/docs/src/external_links.md @@ -13,24 +13,38 @@ * David McCracken's Blog on [Plotly Dash Interactive Mapping - Dash Leaflet & TiTiler](https://www.pywram.com/t/blog-plotly-dash-interactive-mapping-dash-leaflet-titiler/287) +## TiTiler extensions/plugins + +* [stac-utils/titiler-pgstac](https://github.com/stac-utils/titiler-pgstac): TiTiler extension which connects to a PgSTAC database to create dynamic mosaics based on search queries. + +* [developmentseed/titiler-xarray](https://github.com/developmentseed/titiler-xarray): TiTiler extension for xarray + +* [developmentseed/titiler-image](https://github.com/developmentseed/titiler-image): TiTiler demo application for Sentinel-2 Digital Twin dataset + + ## Projects / Demo using TiTiler * ESA Charter Mapper [geobrowser](https://docs.charter.uat.esaportal.eu/webPortal/geobrowser/titiler/) * [developmentseed/titiler-digitaltwin](https://github.com/developmentseed/titiler-digitaltwin): TiTiler demo application for Sentinel-2 Digital Twin dataset -* [developmentseed/titiler-pds](https://github.com/developmentseed/titiler-pds): TiTiler demo application for Sentinel-2 and Landsat-8 AWS Public Datasets - * [developmentseed/titiler-mvt](https://github.com/developmentseed/titiler-mvt): TiTiler demo application to create Mapbox Vector Tiles from COG +* [developmentseed/titiler-pds](https://github.com/developmentseed/titiler-pds): TiTiler demo application for Sentinel-2 and Landsat-8 AWS Public Datasets + * [stac-utils/stac-fastapi](https://github.com/stac-utils/stac-fastapi): STAC API implementation with FastAPI. * [c-core-labs/stac-api](https://github.com/c-core-labs/stac-api): STAC compliant API implementation (built from stac-fastapi) -* [lambgeo/titiler-layer](https://github.com/lambgeo/titiler-layer): TiTiler Lambda layers for easy deployment on AWS +* [developmentseed/titiler-lambda-layer](https://github.com/developmentseed/titiler-lambda-layer): TiTiler Lambda layers for easy deployment on AWS * [Terradue/Stars](https://github.com/Terradue/Stars): Spatio Temporal Asset Runtime Services +* [developmentseed/rio-viz](https://github.com/developmentseed/rio-viz): Visualize Cloud Optimized GeoTIFF in browser + +* [developmentseed/pearl-backend](https://github.com/developmentseed/pearl-backend): PEARL (Planetary Computer Land Cover Mapping) Platform API and Infrastructure + +* [microsoft/planetary-computer-apis](https://github.com/microsoft/planetary-computer-apis): Microsoft Planetary Computer APIs ## Conferences / presentations / videos diff --git a/docs/src/img/api_docs.png b/docs/src/img/api_docs.png new file mode 100644 index 000000000..18724abd4 Binary files /dev/null and b/docs/src/img/api_docs.png differ diff --git a/docs/src/img/browser.png b/docs/src/img/browser.png new file mode 100644 index 000000000..965b6b9ac Binary files /dev/null and b/docs/src/img/browser.png differ diff --git a/docs/src/img/server_logs.png b/docs/src/img/server_logs.png new file mode 100644 index 000000000..90cae500d Binary files /dev/null and b/docs/src/img/server_logs.png differ diff --git a/docs/src/intro.md b/docs/src/intro.md deleted file mode 100644 index 8941f6aa0..000000000 --- a/docs/src/intro.md +++ /dev/null @@ -1,129 +0,0 @@ - -![](https://user-images.githubusercontent.com/10407788/203526990-f58783cf-a288-4801-8fa6-ee511de91a48.png) - -`TiTiler` is a set of python modules whose goal are to help users in creating a dynamic tile server. To learn more about `dynamic tiling` please refer to the [docs](dynamic_tiling.md). - -Users can choose to extend or use `TiTiler` as it is. - -## Default Application - -`TiTiler` comes with a default (complete) application with support for COG, STAC, and MosaicJSON. You can install and start the application locally by doing: - -```bash -# Update pip -python -m pip install -U pip - -# Install titiler packages -python -m pip install uvicorn titiler.application - -# Start application using uvicorn -uvicorn titiler.application.main:app - -> INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -``` - -See default endpoints documentation pages: - -* [`/cog` - Cloud Optimized GeoTIFF](endpoints/cog.md) -* [`/mosaicjson` - MosaicJSON](endpoints/mosaic.md) -* [`/stac` - Spatio Temporal Asset Catalog](endpoints/stac.md) -* [`/tms` - TileMatrixSets](endpoints/tms.md) - -#### Settings - -The default application can be customized using environment variables defined in `titiler.application.settings.ApiSettings` class. Each variable needs to be prefixed with `TITILER_API_`. - -- `NAME` (str): name of the application. Defaults to `titiler`. -- `CORS_ORIGINS` (str, `,` delimited origins): allowed CORS origin. Defaults to `*`. -- `CACHECONTROL` (str): Cache control header to add to responses. Defaults to `"public, max-age=3600"`. -- `ROOT_PATH` (str): path behind proxy. -- `DEBUG` (str): adds `LoggerMiddleware` and `TotalTimeMiddleware` in the middleware stack. -- `DISABLE_COG` (bool): disable `/cog` endpoints. -- `DISABLE_STAC` (bool): disable `/stac` endpoints. -- `DISABLE_MOSAIC` (bool): disable `/mosaic` endpoints. -- `LOWER_CASE_QUERY_PARAMETERS` (bool): transform all query-parameters to lower case (see https://github.com/developmentseed/titiler/pull/321). - -## Customized, minimal app - -`TiTiler` has been developed so users can build their own app using only the portions they need. Using [TilerFactories](advanced/tiler_factories.md), users can create a fully customized application with only the endpoints needed. - -When building a custom application, you may wish to only install the `core` and/or `mosaic` modules. To install these from PyPI: - -```bash -# Update pip -python -m pip install -U pip - -# Install titiler.core and uvicorn packages -python -m pip install titiler.core uvicorn -``` - -These can then be used like: - -```py -# app.py -import uvicorn -from titiler.core.factory import TilerFactory -from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers - -from fastapi import FastAPI - -app = FastAPI() -cog = TilerFactory() -app.include_router(cog.router) -add_exception_handlers(app, DEFAULT_STATUS_CODES) - - -if __name__ == '__main__': - uvicorn.run(app=app, host="127.0.0.1", port=8080, log_level="info") -``` - -![](img/custom_app.png) - -## Extending TiTiler's app - -If you want to include all of Titiler's built-in endpoints, but also include -customized endpoints, you can import and extend the app directly. - -```bash -python -m pip install titiler.application uvicorn # also installs titiler.core and titiler.mosaic -``` - -These can then be used like: - - -```py -# Add private COG endpoints requiring token validation -from fastapi import APIRouter, Depends, HTTPException, Security -from fastapi.security.api_key import APIKeyQuery - -from titiler.application.main import app -from titiler.core.factory import TilerFactory - - -api_key_query = APIKeyQuery(name="access_token", auto_error=False) - - -def token_validation(access_token: str = Security(api_key_query)): - """stupid token validation.""" - if not access_token: - raise HTTPException(status_code=403, detail="Missing `access_token`") - - # if access_token == `token` then OK - if not access_token == "token": - raise HTTPException(status_code=403, detail="Invalid `access_token`") - - return True - - -# Custom router with token dependency -router = APIRouter(dependencies=[Depends(token_validation)]) -tiler = TilerFactory(router_prefix="private/cog", router=router) - -app.include_router(tiler.router, prefix="/private/cog", tags=["Private"]) - - -if __name__ == '__main__': - uvicorn.run(app=app, host="127.0.0.1", port=8080, log_level="info") -``` - -More on [customization](advanced/customization.md) diff --git a/docs/src/mosaics.md b/docs/src/mosaics.md deleted file mode 100644 index 1695bca95..000000000 --- a/docs/src/mosaics.md +++ /dev/null @@ -1,16 +0,0 @@ - -[Work in Progress] - -![](img/africa_mosaic.png) - -`Titiler` has native support for reading and creating web map tiles from **MosaicJSON**. - -> MosaicJSON is an open standard for representing metadata about a mosaic of Cloud-Optimized GeoTIFF (COG) files. - -Ref: https://github.com/developmentseed/mosaicjson-spec - - -### Links - -- https://medium.com/devseed/cog-talk-part-2-mosaics-bbbf474e66df -- https://github.com/developmentseed/cogeo-mosaic diff --git a/docs/src/overrides/partials/integrations/analytics/plausible.html b/docs/src/overrides/partials/integrations/analytics/plausible.html new file mode 100644 index 000000000..828f7d9ab --- /dev/null +++ b/docs/src/overrides/partials/integrations/analytics/plausible.html @@ -0,0 +1,53 @@ + + + + diff --git a/docs/src/packages/application.md b/docs/src/packages/application.md new file mode 120000 index 000000000..9f698a377 --- /dev/null +++ b/docs/src/packages/application.md @@ -0,0 +1 @@ +../../../src/titiler/application/README.md \ No newline at end of file diff --git a/docs/src/packages/core.md b/docs/src/packages/core.md new file mode 120000 index 000000000..fff7ecdd5 --- /dev/null +++ b/docs/src/packages/core.md @@ -0,0 +1 @@ +../../../src/titiler/core/README.md \ No newline at end of file diff --git a/docs/src/packages/extensions.md b/docs/src/packages/extensions.md new file mode 120000 index 000000000..6fcc9e3d1 --- /dev/null +++ b/docs/src/packages/extensions.md @@ -0,0 +1 @@ +../../../src/titiler/extensions/README.md \ No newline at end of file diff --git a/docs/src/packages/mosaic.md b/docs/src/packages/mosaic.md new file mode 120000 index 000000000..cf87cb31c --- /dev/null +++ b/docs/src/packages/mosaic.md @@ -0,0 +1 @@ +../../../src/titiler/mosaic/README.md \ No newline at end of file diff --git a/docs/src/packages/xarray.md b/docs/src/packages/xarray.md new file mode 120000 index 000000000..dc85e70b9 --- /dev/null +++ b/docs/src/packages/xarray.md @@ -0,0 +1 @@ +../../../src/titiler/xarray/README.md \ No newline at end of file diff --git a/docs/src/security.md b/docs/src/security.md new file mode 120000 index 000000000..42cce94fd --- /dev/null +++ b/docs/src/security.md @@ -0,0 +1 @@ +../../SECURITY.md \ No newline at end of file diff --git a/docs/src/advanced/Algorithms.md b/docs/src/user_guide/algorithms.md similarity index 83% rename from docs/src/advanced/Algorithms.md rename to docs/src/user_guide/algorithms.md index eb823e9fb..e5c5c180c 100644 --- a/docs/src/advanced/Algorithms.md +++ b/docs/src/user_guide/algorithms.md @@ -6,11 +6,23 @@ The algorithms are meant to overcome the limitation of `expression` (using [nume We added a set of custom algorithms: -- `hillshade`: Create hillshade from elevation dataset -- `contours`: Create contours lines (raster) from elevation dataset -- `terrarium`: Mapzen's format to encode elevation value in RGB values (https://github.com/tilezen/joerd/blob/master/docs/formats.md#terrarium) -- `terrainrgb`: Mapbox's format to encode elevation value in RGB values (https://docs.mapbox.com/data/tilesets/guides/access-elevation-data/) +- `hillshade`: Create hillshade from elevation dataset (parameters: azimuth (45), angle_altitude(45)) +- `contours`: Create contours lines (raster) from elevation dataset (parameters: increment (35), thickness (1)) +- `slope`: Create degrees of slope from elevation dataset +- `terrarium`: [Mapzen's format](https://github.com/tilezen/joerd/blob/master/docs/formats.md#terrarium) to encode elevation value in RGB values `elevation = (red * 256 + green + blue / 256) - 32768` +- `terrainrgb`: [Mapbox](https://docs.mapbox.com/data/tilesets/guides/access-elevation-data/)/[Maptiler](https://docs.maptiler.com/guides/map-tilling-hosting/data-hosting/rgb-terrain-by-maptiler/)'s format to encode elevation value in RGB values `elevation = -10000 + ((red * 256 * 256 + green * 256 + blue) * 0.1)` - `normalizedIndex`: Normalized Difference Index (e.g NDVI) +- `cast`: Cast data to integer +- `floor`: Round data to the smallest integer +- `ceil`: Round data to the largest integer + +- `min`: Return **Min** values along the `bands` axis. +- `max`: Return **Max** values along the `bands` axis. +- `median`: Return **Median** values along the `bands` axis. +- `mean`: Return **Mean** values along the `bands` axis. +- `std`: Return the **Standard Deviation** along the `bands` axis. +- `var`: Return **Variance** along the `bands` axis. + ### Usage diff --git a/docs/src/dynamic_tiling.md b/docs/src/user_guide/dynamic_tiling.md similarity index 100% rename from docs/src/dynamic_tiling.md rename to docs/src/user_guide/dynamic_tiling.md diff --git a/docs/src/user_guide/getting_started.md b/docs/src/user_guide/getting_started.md new file mode 100644 index 000000000..f962e1793 --- /dev/null +++ b/docs/src/user_guide/getting_started.md @@ -0,0 +1,380 @@ +[TiTiler](https://developmentseed.org/titiler) is a modern map tile server that helps developers quickly serve geospatial data on the web. Think of it as a specialized tool that takes large geographic files (like satellite imagery) and slices them into small, web-friendly map tiles that load efficiently in browser-based maps. + +Built on FastAPI, TiTiler makes working with Cloud-Optimized GeoTIFFs, Spatio Temporal Asset Catalog and other spatial data formats straightforward, even if you're not a GIS expert. It handles all the complex work of processing geographic data and serving it through simple API endpoints that any web developer can use. + +In the past, putting maps on websites was a real pain. Developers had to use bulky tools like GeoServer that were hard to set up, or spend hours making thousands of **static** tiny map images with tools like gdal2tiles that couldn't be changed later. TiTiler makes this so much easier. It creates **dynamic** map pieces right when you need them, instead of making them all beforehand. It works great with modern cloud data and doesn't need complicated setup. This means less headache and more time to focus on building cool map features that users will love. + +## Dynamic vs. Static Tiles: What's the Difference? + +Static tiles are like pre-printed map pieces stored in folders. Once created, they're locked—changing anything means starting over. They use lots of storage but load quickly. + +TiTiler's dynamic tiles work like a chef cooking to order. When someone views your map, TiTiler grabs just the data needed and creates tiles on the spot. This lets you instantly change colors, adjust contrast, or highlight different features. Your map becomes flexible and responsive, adapting to what users need right now rather than being stuck with choices made earlier. + +More on [Dynamic Tiling](dynamic_tiling.md) + +## Let's Get TiTiler Up and Running! + +Now that we understand the advantage of TiTiler's dynamic approach, let's get it running on your local machine. Follow these steps: + +### **1. Create Your Project Workspace** + +First, let's create a dedicated space for our TiTiler project. Open your terminal (Command Prompt or PowerShell on Windows, Terminal on macOS/Linux) and run: + +```bash +# Works on all operating systems +mkdir Titiler +cd Titiler +``` + +> 💡 **Pro Tip**: Keeping your TiTiler project in its own folder makes it easier to manage and prevents conflicts with other Python projects. + +### **2. Set Up a Python Virtual Environment** +a. Create the virtual environment: + ```bash + python -m venv titiler + ``` +b. Activate the virtual environment: + - **For Linux/macOS:** + ```bash + source titiler/bin/activate + ``` + - **For Windows:** + ```bash + titiler\Scripts\activate + ``` + +### **3. Install TiTiler and Its Dependencies** + +With your environment activated, install TiTiler and the web server it needs: + +```bash +pip install titiler.core uvicorn +``` + +This command installs the core TiTiler package and Uvicorn, a lightning-fast ASGI server. + +> 💡 **What's happening**: TiTiler.core contains the essential functionality for serving map tiles. Uvicorn is the engine that will run our FastAPI application. + +### **4. Create Your TiTiler Application** + +Now for the fun part! Create a file named `main.py` with the following code: + +```python +from fastapi import FastAPI +from titiler.core.factory import TilerFactory + +from starlette.middleware.cors import CORSMiddleware + +app = FastAPI() + +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Allows all origins (for development - be more specific in production) + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Create a TilerFactory for Cloud-Optimized GeoTIFFs +cog = TilerFactory() + +# Register all the COG endpoints automatically +app.include_router(cog.router, tags=["Cloud Optimized GeoTIFF"]) + + +# Optional: Add a welcome message for the root endpoint +@app.get("/") +def read_index(): + return {"message": "Welcome to TiTiler"} +``` + +> 💡 **Code Breakdown**: +> +> - We create a FastAPI app and add CORS middleware to allow web maps to access our images +> - The `TilerFactory()` creates all the endpoints needed for serving COG tiles +> - We include those endpoints in our app with `app.include_router()` +> - A simple home endpoint provides a welcome message + +### **5. Launch Your TiTiler Server** + +Run the following command to start the server: +```bash +uvicorn main:app --reload +``` +You should see output similar to this: + +![server logs](../img/server_logs.png) + +> 💡 **The `--reload` flag** automatically restarts the server whenever you change your code - perfect for development! + +### **6. Explore Your TiTiler API** + +Open your browser and go to: + +``` http://127.0.0.1:8000/ ``` - See your welcome message + +![browser](../img/browser.png) + +``` http://127.0.0.1:8000/docs ``` - Explore the interactive API documentation. The `/docs` page is your mission control center. It shows all the endpoints TiTiler created for you and lets you test them directly in your browser: + +![api docs](../img/api_docs.png) + +## Visualizing Your Geospatial Data + +Now that your server is running, let's see what it can do with real data! + +### **Quick Preview of Your Raster** + +To get a quick preview of any Cloud-Optimized GeoTIFF, use: + +```bash +http://127.0.0.1:8000/preview?url=file:///path_to_your_raster.tif +``` +> ⚠️ **Note**: Replace the path with the actual path to your COG file. Remember to use the full path for local files. + +## Visualizing a Specific Tile (Z, X, Y) + +When working with web maps, understanding tile coordinates is essential. Let's break down what Z, X, Y values mean: + +- **Z (zoom level)**: How far in/out you're zoomed. Lower numbers (0-5) show the whole world with less detail; higher numbers (15-22) show smaller areas with more detail. +- **X (column)**: Horizontal position, increasing eastward. +- **Y (row)**: Vertical position, increasing southward. + +At zoom level 0, there's just 1 tile for the whole world. Each zoom level increase splits each tile into 4 more detailed tiles. + +### **Why Visualize Specific Tiles?** + +- **Performance**: Load only what users can see +- **Debugging**: Inspect problematic tiles +- **Specific Analysis**: Extract data from exact locations + +### **Finding Z, X, Y for Your Image** + +The `rio_tiler` and `morecantile` library makes this straightforward: + +```python +from rio_tiler.io import Reader +import morecantile + +# Web Mercator is the default tiling scheme for most web map clients +WEB_MERCATOR_TMS = morecantile.tms.get("WebMercatorQuad") + +with Reader('/path/to/your/raster.tif', tms=WEB_MERCATOR_TMS) as src: + bbox = src.get_geographic_bounds("epsg:4326") + zoom = 15 + # Find all tiles covering the bounding box + tiles = list(src.tms.tiles(bbox[0], bbox[1], bbox[2], bbox[3], zoom)) + for t in tiles: + print("Tile coordinate (x, y, z):", t.x, t.y, t.z) +``` + +### **Viewing a Specific Tile in TiTiler** + +For example, if your tile has coordinates `x=5412, y=12463, z=15`, you would access the specific tile with: + +```bash +http://127.0.0.1:8000/tiles/WebMercatorQuad/15/5412/12463.png?url=raster.tif +``` + +URL components explained: + +- **`WebMercatorQuad/`**: The tiling scheme (this should match your raster's CRS - TiTiler will reproject on-the-fly if needed, but using the correct scheme improves performance and accuracy) +- **`{z}/{x}/{y}`**: Your tile coordinates +- **`.png`**: Output format (alternatives: `.jpg`, `.webp`, `.tif`) +- **`?url=raster.tif`**: Source raster file + +More on [Tiling Schemes](tile_matrix_sets.md) + +### **Creating a Web Map with Leaflet** + +[Leaflet](https://leafletjs.com/) is a lightweight, open-source JavaScript library for interactive maps. It lets you combine base maps (like OpenStreetMap) with overlays from custom tile servers such as TiTiler. + +The following code (in **map.html**) loads a base map, adds your TiTiler raster overlay, and automatically sets the map's view to the raster's bounds: + +--- + +
+ map.html Code + +```html + + + + Leaflet Basemap + TiTiler Raster Overlay + + + + + + +
+ + + +``` +
+ +--- + +## Troubleshooting Common Issues + +### **CORS Issues** + +If you encounter "Access to fetch at X has been blocked by CORS policy" errors in your browser console, make sure you: + +- Have included the CORS middleware in `main.py` as shown above +- Restart your TiTiler server after making changes + +### **File Not Found Errors** + +When using `file:///` URLs: +- Make sure to use the absolute path to your file with the correct format for your operating system: + + - Windows: `file:///C:/Users/username/data/image.tif` + - macOS: `file:///Users/username/data/image.tif` + - Linux: `file:///home/username/data/image.tif` + +### **No Tiles Showing** + +If your map loads but your tiles don't appear: + +- Check the browser console for errors +- Verify that your GeoTIFF is Cloud-Optimized (use `rio cogeo validate` from the rio-cogeo package) +- Try different zoom levels - your data might not be visible at all scales + +--- +*Created by [Dimple Jain](https://jaiindimple.github.io)* + + +## Default Application + +`TiTiler` comes with a default (complete) application with support for COG, STAC, and MosaicJSON. You can install and start the application locally by doing: + +```bash +# Update pip +python -m pip install -U pip + +# Install titiler packages +python -m pip install uvicorn titiler.application + +# Start application using uvicorn +uvicorn titiler.application.main:app + +> INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +See default endpoints documentation pages: + +* [`/cog` - Cloud Optimized GeoTIFF](../endpoints/cog.md) +* [`/mosaicjson` - MosaicJSON](../endpoints/mosaic.md) +* [`/stac` - Spatio Temporal Asset Catalog](../endpoints/stac.md) +* [`/tileMatrixSets` - Tiling Schemes](../endpoints/tms.md) +* [`/algorithms` - Algorithms](../endpoints/algorithms.md) +* [`/colorMaps` - ColorMaps](../endpoints/colormaps.md) + +#### Settings + +The default application can be customized using environment variables defined in `titiler.application.settings.ApiSettings` class. Each variable needs to be prefixed with `TITILER_API_`. + +- `NAME` (str): name of the application. Defaults to `titiler`. +- `CORS_ORIGINS` (str, `,` delimited origins): allowed CORS origin. Defaults to `*`. +- `CORS_ALLOW_METHODS` (str, `,` delimited methods): allowed CORS methods. Defaults to `GET`. +- `CACHECONTROL` (str): Cache control header to add to responses. Defaults to `"public, max-age=3600"`. +- `ROOT_PATH` (str): path behind proxy. +- `DEBUG` (str): adds `LoggerMiddleware` and `TotalTimeMiddleware` in the middleware stack. +- `DISABLE_COG` (bool): disable `/cog` endpoints. +- `DISABLE_STAC` (bool): disable `/stac` endpoints. +- `DISABLE_MOSAIC` (bool): disable `/mosaic` endpoints. +- `LOWER_CASE_QUERY_PARAMETERS` (bool): transform all query-parameters to lower case (see https://github.com/developmentseed/titiler/pull/321). +- `GLOBAL_ACCESS_TOKEN` (str | None): a string which is required in the `?access_token=` query param with every request. + + +#### Extending TiTiler's app + +If you want to include all of Titiler's built-in endpoints, but also include +customized endpoints, you can import and extend the app directly. + +```bash +python -m pip install titiler.application uvicorn # also installs titiler.core and titiler.mosaic +``` + +These can then be used like: + + +```py +# Add private COG endpoints requiring token validation +from fastapi import APIRouter, Depends, HTTPException, Security +from fastapi.security.api_key import APIKeyQuery + +from titiler.application.main import app +from titiler.core.factory import TilerFactory + +import uvicorn + +api_key_query = APIKeyQuery(name="access_token", auto_error=False) + + +def token_validation(access_token: str = Security(api_key_query)): + """stupid token validation.""" + if not access_token: + raise HTTPException(status_code=401, detail="Missing `access_token`") + + # if access_token == `token` then OK + if not access_token == "token": + raise HTTPException(status_code=401, detail="Invalid `access_token`") + + return True + + +# Custom router with token dependency +router = APIRouter(dependencies=[Depends(token_validation)]) +tiler = TilerFactory(router_prefix="private/cog", router=router) + +app.include_router(tiler.router, prefix="/private/cog", tags=["Private"]) + + +if __name__ == '__main__': + uvicorn.run(app=app, host="127.0.0.1", port=8080, log_level="info") +``` + +More on [customization](../advanced/customization.md) diff --git a/docs/src/output_format.md b/docs/src/user_guide/output_format.md similarity index 96% rename from docs/src/output_format.md rename to docs/src/user_guide/output_format.md index 8d32ad577..2055ff931 100644 --- a/docs/src/output_format.md +++ b/docs/src/user_guide/output_format.md @@ -47,7 +47,7 @@ print(data.shape) data, mask = data[0:-1], data[-1] ``` -Notebook: [Working_with_NumpyTile](examples/notebooks/Working_with_NumpyTile.ipynb) +Notebook: [Working_with_NumpyTile](../examples/notebooks/Working_with_NumpyTile.ipynb) ## JSONResponse diff --git a/docs/src/advanced/rendering.md b/docs/src/user_guide/rendering.md similarity index 71% rename from docs/src/advanced/rendering.md rename to docs/src/user_guide/rendering.md index ea31bfd3a..417151fb3 100644 --- a/docs/src/advanced/rendering.md +++ b/docs/src/user_guide/rendering.md @@ -16,19 +16,22 @@ Titiler supports both default colormaps (each with a name) and custom color maps ### Default Colormaps -Default colormaps pre-made, each with a given name. These maps come from the `rio-tiler` library, which has taken colormaps packaged with Matplotlib and has added others that are commonly used with raster data. +Default colormaps pre-made, each with a given name. These maps come from the `rio-tiler` library, which has taken colormaps packaged with Matplotlib and has added others that are commonly used with raster data. A list of available color maps can be found in Titiler's Swagger docs, or in the [rio-tiler documentation](https://cogeotiff.github.io/rio-tiler/colormap/#default-rio-tilers-colormaps). To use a default colormap, simply use the parameter `colormap_name`: -```python3 -import requests +```python +import httpx -resp = requests.get("titiler.xyz/cog/preview", params={ - "url": "", - "colormap_name": "" # e.g. autumn_r -}) +resp = httpx.get( + "https://titiler.xyz/cog/preview", + params={ + "url": "", + "colormap_name": "" # e.g. autumn_r + } +) ``` You can take any of the colormaps listed on `rio-tiler`, and add `_r` to reverse it. @@ -37,19 +40,19 @@ You can take any of the colormaps listed on `rio-tiler`, and add `_r` to reverse If you'd like to specify your own colormap, you can specify your own using an encoded JSON: -```python3 -import requests +```python +import httpx -response = requests.get( - f"titiler.xyz/cog/preview", +response = httpx.get( + "https://titiler.xyz/cog/preview", params={ - "url": "", + "url": "", "bidx": "1", - "colormap": { - "0": "#e5f5f9", - "10": "#99d8c9", - "255": "#2ca25f", - } + "colormap": json.dumps({ + "0": "#e5f5f9", + "10": "#99d8c9", + "255": "#2ca25f", + }) } ) ``` @@ -58,7 +61,7 @@ Titiler supports colormaps that are both discrete (where pixels will be one of t For more information, please check out [rio-tiler's docs](https://cogeotiff.github.io/rio-tiler/colormap/). -It is also possible to add a [colormap dependency](../../examples/code/tiler_with_custom_colormap) to automatically apply +It is also possible to add a [colormap dependency](../examples/code/tiler_with_custom_colormap) to automatically apply a default colormap. ## Color Formula @@ -75,13 +78,13 @@ Titiler supports color formulae as defined in [Mapbox's `rio-color` plugin](http In Titiler, color_formulae are applied through the `color_formula` parameter as a string. An example of this option in action: -```python3 -import requests +```python +import httpx -response = requests.get( - f"titiler.xyz/cog/preview", +response = httpx.get( + "https://titiler.xyz/cog/preview", params={ - "url": "", + "url": "", "color_formula": "gamma rg 1.3, sigmoidal rgb 22 0.1, saturation 1.5" } ) @@ -91,25 +94,41 @@ response = requests.get( Rescaling is the act of adjusting the minimum and maximum values when rendering an image. In an image with a single band, the rescaled minimum value will be set to black, and the rescaled maximum value will be set to white. This is useful if you want to accentuate features that only appear at a certain pixel value (e.g. you have a DEM, but you want to highlight how the terrain changes between sea level and 100m). -Titiler supports rescaling on a per-band basis, using the `rescaling` parameter. The input is a list of comma-delimited min-max ranges (e.g. ["0,100", "100,200", "0,1000]). +All titiler endpoinds returning *image* support `rescale` parameter. The parameter should be in form of `"rescale={min},{max}"`. -```python3 -import requests +```python +import httpx -response = requests.get( - f"titiler.xyz/cog/preview", +response = httpx.get( + "https;//titiler.xyz/cog/preview", params={ - "url": "", - "rescaling": ["0,100", "0,1000", "0,10000"] - } + "url": "", + "rescale": "0,100", + }, +) +``` + +Titiler supports rescaling on a per-band basis, using multiple `rescale` parameters. + +```python +import httpx + +response = httpx.get( + "https;//titiler.xyz/cog/preview", + params=( + ("url", ""), + ("rescale", "0,100"), + ("rescale", "0,1000"), + ("rescale", "0,10000"), + ), ) ``` By default, Titiler will rescale the bands using the min/max values of the input datatype. For example, PNG images 8- or 16-bit unsigned pixels, -giving a possible range of 0 to 255 or 0 to 65,536, so Titiler will use these ranges to rescale to the output format. +giving a possible range of 0 to 255 or 0 to 65,536, so Titiler will use these ranges to rescale to the output format. -For certain datasets (e.g. DEMs) this default behaviour can make the image seem washed out (or even entirely one color), +For certain datasets (e.g. DEMs) this default behaviour can make the image seem washed out (or even entirely one color), so if you see this happen look into rescaling your images to something that makes sense for your data. -It is also possible to add a [rescaling dependency](../../api/titiler/core/dependencies/#rescalingparams) to automatically apply -a default rescale. \ No newline at end of file +It is also possible to add a [rescaling dependency](../api/titiler/core/dependencies/#ImageRenderingParams) to automatically apply +a default rescale. diff --git a/docs/src/tile_matrix_sets.md b/docs/src/user_guide/tile_matrix_sets.md similarity index 91% rename from docs/src/tile_matrix_sets.md rename to docs/src/user_guide/tile_matrix_sets.md index eea52d992..3488ff54e 100644 --- a/docs/src/tile_matrix_sets.md +++ b/docs/src/user_guide/tile_matrix_sets.md @@ -26,7 +26,7 @@ $ curl http://127.0.0.1:8000/tileMatrixSets | jq '.tileMatrixSets[] | .id' "WebMercatorQuad" ``` -You can easily add more TileMatrixSet support, see [custom tms](advanced/customization.md#custom-tms). +You can easily add more TileMatrixSet support, see [custom tms](../advanced/customization.md#custom-tms). -Notebook: [Working_with_nonWebMercatorTMS](examples/notebooks/Working_with_nonWebMercatorTMS.ipynb) +Notebook: [Working_with_nonWebMercatorTMS](../examples/notebooks/Working_with_nonWebMercatorTMS.ipynb) diff --git a/pyproject.toml b/pyproject.toml index b691b6110..3df36bad8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "titiler" description = "A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" license = {file = "LICENSE"} authors = [ {name = "Vincent Sarago", email = "vincent@developmentseed.com"}, @@ -21,31 +21,21 @@ classifiers = [ "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: GIS", ] -version="0.11.7" +version="0.22.4" dependencies = [ - "titiler.core==0.11.7", - "titiler.extensions==0.11.7", - "titiler.mosaic==0.11.7", - "titiler.application==0.11.7", -] - -[project.optional-dependencies] -dev = [ - "pre-commit", -] -docs = [ - "nbconvert", - "mkdocs", - "mkdocs-jupyter", - "mkdocs-material", - "pygments", - "pdocs", + "titiler.core==0.22.4", + "titiler.xarray==0.22.4", + "titiler.extensions==0.22.4", + "titiler.mosaic==0.22.4", + "titiler.application==0.22.4", ] [project.urls] @@ -80,7 +70,7 @@ exclude = [ ] [build-system] -requires = ["hatchling"] +requires = ["hatchling>=1.12.0"] build-backend = "hatchling.build" [tool.coverage.run] @@ -107,7 +97,7 @@ known_third_party = [ ] default_section = "THIRDPARTY" -[tool.ruff] +[tool.ruff.lint] select = [ "D1", # pydocstyle errors "E", # pycodestyle errors @@ -121,9 +111,177 @@ ignore = [ "B008", # do not perform function calls in argument defaults "B905", # ignore zip() without an explicit strict= parameter, only support with python >3.10 ] +exclude = [ + "*.ipynb" +] [tool.mypy] no_implicit_optional = true strict_optional = true namespace_packages = true explicit_package_bases = true + +[tool.pytest.ini_options] +filterwarnings = [ + "ignore::rasterio.errors.NotGeoreferencedWarning", +] + + + + + + + +[tool.hatch.build.targets.wheel] +bypass-selection = true + +[tool.bumpversion] +current_version = "0.22.4" +parse = """(?x) + (?P\\d+)\\. + (?P\\d+)\\. + (?P\\d+) + (?: + (?Pa|b|rc) # pre-release label + (?P\\d+) # pre-release version number + )? # pre-release section is optional + (?: + \\.post + (?P\\d+) # post-release version number + )? # post-release section is optional +""" +serialize = [ + "{major}.{minor}.{patch}.post{post_n}", + "{major}.{minor}.{patch}{pre_l}{pre_n}", + "{major}.{minor}.{patch}", +] + +search = "{current_version}" +replace = "{new_version}" +regex = false +tag = false +commit = false +allow_dirty = true +tag_name = "{new_version}" + +############################################################################### +# update titiler meta package +[[tool.bumpversion.files]] +filename = "pyproject.toml" +search = 'version="{current_version}"' +replace = 'version="{new_version}"' + +# core +[[tool.bumpversion.files]] +filename = "pyproject.toml" +search = 'titiler.core=={current_version}' +replace = 'titiler.core=={new_version}' + +# xarray +[[tool.bumpversion.files]] +filename = "pyproject.toml" +search = 'titiler.xarray=={current_version}' +replace = 'titiler.xarray=={new_version}' + +# extensions +[[tool.bumpversion.files]] +filename = "pyproject.toml" +search = 'titiler.extensions=={current_version}' +replace = 'titiler.extensions=={new_version}' + +# mosaic +[[tool.bumpversion.files]] +filename = "pyproject.toml" +search = 'titiler.mosaic=={current_version}' +replace = 'titiler.mosaic=={new_version}' + +# application +[[tool.bumpversion.files]] +filename = "pyproject.toml" +search = 'titiler.application=={current_version}' +replace = 'titiler.application=={new_version}' + +############################################################################### +# Update sub modules version +# titiler.core +[[tool.bumpversion.files]] +filename = "src/titiler/core/titiler/core/__init__.py" +search = '__version__ = "{current_version}"' +replace = '__version__ = "{new_version}"' + +# titiler.xarray +[[tool.bumpversion.files]] +filename = "src/titiler/xarray/titiler/xarray/__init__.py" +search = '__version__ = "{current_version}"' +replace = '__version__ = "{new_version}"' + +# titiler.extensions +[[tool.bumpversion.files]] +filename = "src/titiler/extensions/titiler/extensions/__init__.py" +search = '__version__ = "{current_version}"' +replace = '__version__ = "{new_version}"' + +# titiler.mosaic +[[tool.bumpversion.files]] +filename = "src/titiler/mosaic/titiler/mosaic/__init__.py" +search = '__version__ = "{current_version}"' +replace = '__version__ = "{new_version}"' + +# titiler.application +[[tool.bumpversion.files]] +filename = "src/titiler/application/titiler/application/__init__.py" +search = '__version__ = "{current_version}"' +replace = '__version__ = "{new_version}"' + +############################################################################### +# Update sub modules dependencies +# titiler.xarray +[[tool.bumpversion.files]] +filename = "src/titiler/xarray/pyproject.toml" +search = 'titiler.core=={current_version}' +replace = 'titiler.core=={new_version}' + +# titiler.extensions +[[tool.bumpversion.files]] +filename = "src/titiler/extensions/pyproject.toml" +search = 'titiler.core=={current_version}' +replace = 'titiler.core=={new_version}' + +# titiler.mosaic +[[tool.bumpversion.files]] +filename = "src/titiler/mosaic/pyproject.toml" +search = 'titiler.core=={current_version}' +replace = 'titiler.core=={new_version}' + +# titiler.application +[[tool.bumpversion.files]] +filename = "src/titiler/application/pyproject.toml" +search = 'titiler.core=={current_version}' +replace = 'titiler.core=={new_version}' + +[[tool.bumpversion.files]] +filename = "src/titiler/application/pyproject.toml" +search = 'titiler.extensions[cogeo,stac]=={current_version}' +replace = 'titiler.extensions[cogeo,stac]=={new_version}' + +[[tool.bumpversion.files]] +filename = "src/titiler/application/pyproject.toml" +search = 'titiler.mosaic=={current_version}' +replace = 'titiler.mosaic=={new_version}' + +############################################################################### +# Others +[[tool.bumpversion.files]] +filename = "deployment/aws/lambda/Dockerfile" +search = 'titiler.application=={current_version}' +replace = 'titiler.application=={new_version}' + +[[tool.bumpversion.files]] +filename = "deployment/aws/lambda/Dockerfile.xarray" +search = 'titiler.xarray=={current_version}' +replace = 'titiler.xarray=={new_version}' + +[[tool.bumpversion.files]] +filename = "deployment/k8s/charts/Chart.yaml" +search = 'appVersion: {current_version}' +replace = 'appVersion: {new_version}' diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt new file mode 100644 index 000000000..06136a824 --- /dev/null +++ b/requirements/requirements-dev.txt @@ -0,0 +1,2 @@ +pre-commit +bump-my-version diff --git a/requirements/requirements-docs.txt b/requirements/requirements-docs.txt new file mode 100644 index 000000000..8fcfa9aa1 --- /dev/null +++ b/requirements/requirements-docs.txt @@ -0,0 +1,6 @@ +black>=23.10.1 +mkdocs>=1.4.3 +mkdocs-jupyter>=0.24.5 +mkdocs-material[imaging]>=9.5 +griffe-inherited-docstrings>=1.0.0 +mkdocstrings[python]>=0.25.1 diff --git a/scripts/publish b/scripts/publish index 7e548c6ec..1383cd75f 100755 --- a/scripts/publish +++ b/scripts/publish @@ -2,6 +2,7 @@ SUBPACKAGE_DIRS=( "core" + "xarray" "mosaic" "application" "extensions" diff --git a/scripts/test b/scripts/test new file mode 100755 index 000000000..db3cc55b0 --- /dev/null +++ b/scripts/test @@ -0,0 +1,17 @@ +#! /usr/bin/env bash + +SUBPACKAGE_DIRS=( + "core" + "xarray" + "mosaic" + "application" + "extensions" +) + +for PACKAGE_DIR in "${SUBPACKAGE_DIRS[@]}" +do + echo "Running tests for titiler-${PACKAGE_DIR}" + pushd ./src/titiler/${PACKAGE_DIR} + python -m pytest -s + popd +done diff --git a/src/titiler/application/LICENSE b/src/titiler/application/LICENSE new file mode 100644 index 000000000..eb4365951 --- /dev/null +++ b/src/titiler/application/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Development Seed + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/titiler/application/README.md b/src/titiler/application/README.md index d8ca172eb..166c10dc3 100644 --- a/src/titiler/application/README.md +++ b/src/titiler/application/README.md @@ -6,19 +6,19 @@ ## Installation ```bash -$ pip install -U pip +python -m pip install -U pip # From Pypi -$ pip install titiler.application +python -m pip install titiler.application # Or from sources -$ git clone https://github.com/developmentseed/titiler.git -$ cd titiler && pip install -e titiler/application +git clone https://github.com/developmentseed/titiler.git +cd titiler && python -m pip install -e src/titiler/core -e src/titiler/extensions -e src/titiler/mosaic -e src/titiler/application ``` Launch Application ```bash -$ pip install uvicorn +$ python -m pip install uvicorn $ uvicorn titiler.application.main:app --reload ``` @@ -30,9 +30,7 @@ titiler/ ├── tests/ - Tests suite └── titiler/application/ - `application` namespace package ├── templates/ - | ├── index.html - demo landing page - | ├── cog_index.html - demo viewer for `/cog` - | └── stac_index.html - demo viewer for `/stac` + | └── index.html - Landing page ├── main.py - Main FastAPI application └── settings.py - demo settings (cache, cors...) ``` diff --git a/src/titiler/application/pyproject.toml b/src/titiler/application/pyproject.toml index f88aa0750..43106e204 100644 --- a/src/titiler/application/pyproject.toml +++ b/src/titiler/application/pyproject.toml @@ -1,8 +1,8 @@ [project] -name = "titiler.application" +name = "titiler-application" description = "A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" authors = [ {name = "Vincent Sarago", email = "vincent@developmentseed.com"}, ] @@ -21,19 +21,21 @@ classifiers = [ "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: GIS", ] dynamic = ["version"] dependencies = [ - "titiler.core==0.11.7", - "titiler.extensions[cogeo,stac]==0.11.7", - "titiler.mosaic==0.11.7", - "starlette-cramjam>=0.3,<0.4", - "python-dotenv", + "titiler.core[telemetry]==0.22.4", + "titiler.extensions[cogeo,stac]==0.22.4", + "titiler.mosaic==0.22.4", + "starlette-cramjam>=0.4,<0.5", + "pydantic-settings~=2.0", ] [project.optional-dependencies] @@ -43,9 +45,10 @@ test = [ "pytest-asyncio", "httpx", "brotlipy", + "boto3", ] server = [ - "uvicorn[standard]>=0.12.0,<0.19.0", + "uvicorn[standard]>=0.12.0", ] [project.urls] @@ -56,8 +59,8 @@ Source = "https://github.com/developmentseed/titiler" Changelog = "https://developmentseed.org/titiler/release-notes/" [build-system] -requires = ["pdm-pep517"] -build-backend = "pdm.pep517.api" +requires = ["pdm-backend"] +build-backend = "pdm.backend" [tool.pdm.version] source = "file" diff --git a/src/titiler/application/tests/conftest.py b/src/titiler/application/tests/conftest.py index 967990102..b883ab570 100644 --- a/src/titiler/application/tests/conftest.py +++ b/src/titiler/application/tests/conftest.py @@ -34,6 +34,7 @@ def app(set_env) -> TestClient: def mock_RequestGet(src_path): """Mock Requests.""" + # HTTP class MockResponse: def __init__(self, data): diff --git a/src/titiler/application/tests/fixtures/item.json b/src/titiler/application/tests/fixtures/item.json index f8e447265..8a20a9ed5 100644 --- a/src/titiler/application/tests/fixtures/item.json +++ b/src/titiler/application/tests/fixtures/item.json @@ -1,18 +1,10 @@ { "type": "Feature", - "stac_version": "1.0.0-beta.1", - "stac_extensions": [ - "eo", - "view", - "proj" - ], + "stac_version": "1.0.0", "id": "S2A_34SGA_20200318_0_L2A", - "bbox": [ - 23.293255090449595, - 31.505183020453355, - 24.296453548295318, - 32.51147809805106 - ], + "properties": { + "datetime": "2020-03-18T09:11:33Z" + }, "geometry": { "type": "Polygon", "coordinates": [ @@ -40,91 +32,31 @@ ] ] }, - "properties": { - "datetime": "2020-03-18T09:11:33Z", - "platform": "sentinel-2a", - "constellation": "sentinel-2", - "instruments": [ - "msi" - ], - "gsd": 10, - "data_coverage": 73.85, - "view:off_nadir": 0, - "eo:cloud_cover": 89.84, - "proj:epsg": 32634, - "sentinel:latitude_band": "S", - "sentinel:grid_square": "GA", - "sentinel:sequence": "0", - "sentinel:product_id": "S2A_MSIL2A_20200318T085701_N0214_R007_T34SGA_20200318T115254", - "created": "2020-05-12T21:03:26.671Z", - "updated": "2020-05-12T21:03:26.671Z" - }, - "collection": "sentinel-s2-l2a-cogs", + "links": [ + { + "rel": "self", + "href": "https://myurl.com/item.json", + "type": "application/json" + } + ], "assets": { "B01": { - "title": "Band 1 (coastal)", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", "href": "https://myurl.com/B01.tif", - "proj:shape": [ - 1830, - 1830 - ], - "proj:transform": [ - 60, - 0, - 699960, - 0, - -60, - 3600000, - 0, - 0, - 1 - ] + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Band 1 (coastal)" }, "B09": { - "title": "Band 9", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", "href": "https://myurl.com/B09.tif", - "proj:shape": [ - 1830, - 1830 - ], - "proj:transform": [ - 60, - 0, - 699960, - 0, - -60, - 3600000, - 0, - 0, - 1 - ] + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Band 9" } }, - "links": [ - { - "rel": "self", - "href": "s3://sentinel-cogs/sentinel-s2-l2a-cogs/2020/S2A_34SGA_20200318_0_L2A/S2A_34SGA_20200318_0_L2A.json", - "type": "application/json" - }, - { - "rel": "parent", - "href": "https://myurl.com/v0/collections/sentinel-s2-l2a" - }, - { - "rel": "collection", - "href": "https://myurl.com/v0/collections/sentinel-s2-l2a" - }, - { - "rel": "root", - "href": "https://myurl.com/v0/" - }, - { - "title": "Source STAC Item", - "rel": "derived_from", - "href": "https://myurl.com/collections/sentinel-s2-l2a/items/S2A_34SGA_20200318_0_L2A", - "type": "application/json" - } - ] + "bbox": [ + 23.293255090449595, + 31.505183020453355, + 24.296453548295318, + 32.51147809805106 + ], + "stac_extensions": [], + "collection": "sentinel-s2-l2a-cogs" } diff --git a/src/titiler/application/tests/routes/test_cog.py b/src/titiler/application/tests/routes/test_cog.py index bacd1cae2..75c30a022 100644 --- a/src/titiler/application/tests/routes/test_cog.py +++ b/src/titiler/application/tests/routes/test_cog.py @@ -4,7 +4,7 @@ import os from io import BytesIO from unittest.mock import patch -from urllib.parse import parse_qsl, urlencode, urlparse +from urllib.parse import parse_qsl, urlparse import numpy import pytest @@ -54,22 +54,28 @@ def test_wmts(rio, app): """test wmts endpoints.""" rio.open = mock_rasterio_open - response = app.get("/cog/WMTSCapabilities.xml?url=https://myurl.com/cog.tif") + response = app.get( + "/cog/WebMercatorQuad/WMTSCapabilities.xml?url=https://myurl.com/cog.tif" + ) assert response.status_code == 200 assert response.headers["content-type"] == "application/xml" assert response.headers["Cache-Control"] == "private, max-age=3600" assert ( - "http://testserver/cog/WMTSCapabilities.xml?url=https" + "http://testserver/cog/WebMercatorQuad/WMTSCapabilities.xml?url=https" in response.content.decode() ) - assert "cogeo" in response.content.decode() + assert "default" in response.content.decode() assert ( "http://testserver/cog/tiles/WebMercatorQuad/{TileMatrix}/{TileCol}/{TileRow}@1x.png?url=https" in response.content.decode() ) + assert ( + "http://www.opengis.net/def/crs/EPSG/0/3857" + in response.content.decode() + ) response = app.get( - "/cog/WMTSCapabilities.xml?url=https://myurl.com/cog.tif&tile_scale=2&tile_format=jpg" + "/cog/WebMercatorQuad/WMTSCapabilities.xml?url=https://myurl.com/cog.tif&tile_scale=2&tile_format=jpg" ) assert response.status_code == 200 assert response.headers["content-type"] == "application/xml" @@ -78,6 +84,13 @@ def test_wmts(rio, app): in response.content.decode() ) + response = app.get( + "/cog/WebMercatorQuad/WMTSCapabilities.xml?url=https://myurl.com/cog.tif&use_epsg=true" + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "application/xml" + assert "EPSG:3857" in response.content.decode() + @patch("rio_tiler.io.rasterio.rasterio") def test_tile(rio, app): @@ -86,7 +99,7 @@ def test_tile(rio, app): # full tile response = app.get( - "/cog/tiles/8/87/48?url=https://myurl.com/cog.tif&rescale=0,1000" + "/cog/tiles/WebMercatorQuad/8/87/48?url=https://myurl.com/cog.tif&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" @@ -96,7 +109,7 @@ def test_tile(rio, app): assert meta["height"] == 256 response = app.get( - "/cog/tiles/8/87/48@2x?url=https://myurl.com/cog.tif&rescale=0,1000&color_formula=Gamma R 3" + "/cog/tiles/WebMercatorQuad/8/87/48@2x?url=https://myurl.com/cog.tif&rescale=0,1000&color_formula=Gamma R 3" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" @@ -105,25 +118,25 @@ def test_tile(rio, app): assert meta["height"] == 512 response = app.get( - "/cog/tiles/8/87/48.jpg?url=https://myurl.com/cog.tif&rescale=0,1000" + "/cog/tiles/WebMercatorQuad/8/87/48.jpg?url=https://myurl.com/cog.tif&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpg" response = app.get( - "/cog/tiles/8/87/48.jpeg?url=https://myurl.com/cog.tif&rescale=0,1000" + "/cog/tiles/WebMercatorQuad/8/87/48.jpeg?url=https://myurl.com/cog.tif&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" response = app.get( - "/cog/tiles/8/87/48@2x.jpg?url=https://myurl.com/cog.tif&rescale=0,1000" + "/cog/tiles/WebMercatorQuad/8/87/48@2x.jpg?url=https://myurl.com/cog.tif&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpg" response = app.get( - "/cog/tiles/8/87/48@2x.tif?url=https://myurl.com/cog.tif&nodata=0&bidx=1" + "/cog/tiles/WebMercatorQuad/8/87/48@2x.tif?url=https://myurl.com/cog.tif&nodata=0&bidx=1" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/tiff; application=geotiff" @@ -133,14 +146,16 @@ def test_tile(rio, app): assert meta["width"] == 512 assert meta["height"] == 512 - response = app.get("/cog/tiles/8/87/48.npy?url=https://myurl.com/cog.tif&nodata=0") + response = app.get( + "/cog/tiles/WebMercatorQuad/8/87/48.npy?url=https://myurl.com/cog.tif&nodata=0" + ) assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" data = numpy.load(BytesIO(response.content)) assert data.shape == (2, 256, 256) response = app.get( - "/cog/tiles/8/87/48.npy?url=https://myurl.com/cog.tif&nodata=0&return_mask=false" + "/cog/tiles/WebMercatorQuad/8/87/48.npy?url=https://myurl.com/cog.tif&nodata=0&return_mask=false" ) assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" @@ -150,7 +165,7 @@ def test_tile(rio, app): # Test brotli compression headers = {"Accept-Encoding": "br"} response = app.get( - "/cog/tiles/8/87/48.npy?url=https://myurl.com/cog.tif&nodata=0&return_mask=false", + "/cog/tiles/WebMercatorQuad/8/87/48.npy?url=https://myurl.com/cog.tif&nodata=0&return_mask=false", headers=headers, ) assert response.status_code == 200 @@ -159,7 +174,7 @@ def test_tile(rio, app): # Exclude png from compression middleware headers = {"Accept-Encoding": "br"} response = app.get( - "/cog/tiles/8/87/48.png?url=https://myurl.com/cog.tif&nodata=0&return_mask=false", + "/cog/tiles/WebMercatorQuad/8/87/48.png?url=https://myurl.com/cog.tif&nodata=0&return_mask=false", headers=headers, ) assert response.status_code == 200 @@ -168,7 +183,7 @@ def test_tile(rio, app): # Test gzip fallback headers = {"Accept-Encoding": "gzip"} response = app.get( - "/cog/tiles/8/87/48.npy?url=https://myurl.com/cog.tif&nodata=0&return_mask=false", + "/cog/tiles/WebMercatorQuad/8/87/48.npy?url=https://myurl.com/cog.tif&nodata=0&return_mask=false", headers=headers, ) assert response.status_code == 200 @@ -176,19 +191,23 @@ def test_tile(rio, app): # partial response = app.get( - "/cog/tiles/8/84/47?url=https://myurl.com/cog.tif&rescale=0,1000" + "/cog/tiles/WebMercatorQuad/8/84/47?url=https://myurl.com/cog.tif&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" response = app.get( - "/cog/tiles/8/84/47?url=https://myurl.com/cog.tif&nodata=0&rescale=0,1000&colormap_name=viridis" + "/cog/tiles/WebMercatorQuad/8/84/47?url=https://myurl.com/cog.tif&nodata=0&rescale=0,1000&colormap_name=viridis" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" - cmap = urlencode( - { + # valid colormap + response = app.get( + "/cog/tiles/WebMercatorQuad/8/53/50.png", + params={ + "url": "https://myurl.com/above_cog.tif", + "bidx": 1, "colormap": json.dumps( { "1": [58, 102, 24, 255], @@ -196,34 +215,31 @@ def test_tile(rio, app): "3": "#b1b129", "4": "#ddcb9aFF", } - ) - } - ) - response = app.get( - f"/cog/tiles/8/53/50.png?url=https://myurl.com/above_cog.tif&bidx=1&{cmap}" + ), + }, ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" - cmap = urlencode({"colormap": json.dumps({"1": [58, 102]})}) + # invalid colormap shape response = app.get( - f"/cog/tiles/8/53/50.png?url=https://myurl.com/above_cog.tif&bidx=1&{cmap}" + "/cog/tiles/WebMercatorQuad/8/53/50.png", + params={ + "url": "https://myurl.com/above_cog.tif", + "bidx": 1, + "colormap": json.dumps({"1": [58, 102]}), + }, ) assert response.status_code == 400 - cmap = urlencode({"colormap": {"1": "#ddcb9aFF"}}) + # bad resampling response = app.get( - f"/cog/tiles/8/53/50.png?url=https://myurl.com/above_cog.tif&bidx=1&{cmap}" - ) - assert response.status_code == 400 - - response = app.get( - "/cog/tiles/8/53/50.png?url=https://myurl.com/above_cog.tif&bidx=1&resampling=somethingwrong" + "/cog/tiles/WebMercatorQuad/8/53/50.png?url=https://myurl.com/above_cog.tif&bidx=1&resampling=somethingwrong" ) assert response.status_code == 422 response = app.get( - "/cog/tiles/8/87/48@2x.tif?url=https://myurl.com/cog.tif&nodata=0&bidx=1&return_mask=false" + "/cog/tiles/WebMercatorQuad/8/87/48@2x.tif?url=https://myurl.com/cog.tif&nodata=0&bidx=1&return_mask=false" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/tiff; application=geotiff" @@ -239,7 +255,9 @@ def test_tilejson(rio, app): """test /tilejson endpoint.""" rio.open = mock_rasterio_open - response = app.get("/cog/tilejson.json?url=https://myurl.com/cog.tif") + response = app.get( + "/cog/WebMercatorQuad/tilejson.json?url=https://myurl.com/cog.tif" + ) assert response.status_code == 200 body = response.json() assert body["tilejson"] == "2.2.0" @@ -255,7 +273,7 @@ def test_tilejson(rio, app): assert body["center"] response = app.get( - "/cog/tilejson.json?url=https://myurl.com/cog.tif&tile_format=png&tile_scale=2" + "/cog/WebMercatorQuad/tilejson.json?url=https://myurl.com/cog.tif&tile_format=png&tile_scale=2" ) assert response.status_code == 200 body = response.json() @@ -269,9 +287,13 @@ def test_tilejson(rio, app): "3": "#b1b129", "4": "#ddcb9aFF", } - cmap = urlencode({"colormap": json.dumps(cmap_dict)}) response = app.get( - f"/cog/tilejson.json?url=https://myurl.com/above_cog.tif&bidx=1&{cmap}" + "/cog/WebMercatorQuad/tilejson.json", + params={ + "url": "https://myurl.com/above_cog.tif", + "bidx": 1, + "colormap": json.dumps(cmap_dict), + }, ) assert response.status_code == 200 body = response.json() @@ -340,11 +362,11 @@ def test_preview(rio, app): @patch("rio_tiler.io.rasterio.rasterio") def test_part(rio, app): - """test /crop endpoint.""" + """test /bbox endpoint.""" rio.open = mock_rasterio_open response = app.get( - "/cog/crop/-56.228,72.715,-54.547,73.188.png?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256" + "/cog/bbox/-56.228,72.715,-54.547,73.188.png?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" @@ -355,7 +377,7 @@ def test_part(rio, app): assert meta["driver"] == "PNG" response = app.get( - "/cog/crop/-56.228,72.715,-54.547,73.188.jpg?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256&return_mask=false" + "/cog/bbox/-56.228,72.715,-54.547,73.188.jpg?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256&return_mask=false" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpg" @@ -366,7 +388,7 @@ def test_part(rio, app): assert meta["driver"] == "JPEG" response = app.get( - "/cog/crop/-56.228,72.715,-54.547,73.188/128x128.png?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256" + "/cog/bbox/-56.228,72.715,-54.547,73.188/128x128.png?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" @@ -376,7 +398,7 @@ def test_part(rio, app): assert meta["driver"] == "PNG" response = app.get( - "/cog/crop/-56.228,72.715,-54.547,73.188.png?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256&width=512&height=512" + "/cog/bbox/-56.228,72.715,-54.547,73.188.png?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256&width=512&height=512" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" @@ -386,7 +408,7 @@ def test_part(rio, app): assert meta["driver"] == "PNG" response = app.get( - "/cog/crop/-56.228,72.715,-54.547,73.188.npy?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256" + "/cog/bbox/-56.228,72.715,-54.547,73.188.npy?url=https://myurl.com/cog.tif&rescale=0,1000&max_size=256" ) assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" @@ -417,7 +439,9 @@ def test_tile_outside_bounds_error(rio, app): """raise 404 when tile is not found.""" rio.open = mock_rasterio_open - response = app.get("/cog/tiles/15/0/0?url=https://myurl.com/cog.tif&rescale=0,1000") + response = app.get( + "/cog/tiles/WebMercatorQuad/15/0/0?url=https://myurl.com/cog.tif&rescale=0,1000" + ) assert response.status_code == 404 assert response.headers["Cache-Control"] == "private, max-age=3600" diff --git a/src/titiler/application/tests/routes/test_mosaic.py b/src/titiler/application/tests/routes/test_mosaic.py index 3e092329d..d9acea550 100644 --- a/src/titiler/application/tests/routes/test_mosaic.py +++ b/src/titiler/application/tests/routes/test_mosaic.py @@ -4,7 +4,7 @@ from typing import Any, Callable from unittest.mock import patch -import mercantile +import morecantile from cogeo_mosaic.backends import FileBackend from cogeo_mosaic.mosaic import MosaicJSON @@ -57,26 +57,30 @@ def test_info(app): response = app.get("/mosaicjson/info", params={"url": MOSAICJSON_FILE}) assert response.status_code == 200 body = response.json() - assert body["minzoom"] == 7 - assert body["maxzoom"] == 9 assert body["name"] == "mosaic" # mosaic.name is not set assert body["quadkeys"] == [] + assert body["mosaic_minzoom"] == 7 + assert body["mosaic_maxzoom"] == 9 + assert body["mosaic_tilematrixset"] response = app.get("/mosaicjson/info.geojson", params={"url": MOSAICJSON_FILE}) assert response.status_code == 200 assert response.headers["content-type"] == "application/geo+json" body = response.json() assert body["geometry"] - assert body["properties"]["minzoom"] == 7 - assert body["properties"]["maxzoom"] == 9 assert body["properties"]["name"] == "mosaic" # mosaic.name is not set assert body["properties"]["quadkeys"] == [] + assert body["properties"]["mosaic_minzoom"] == 7 + assert body["properties"]["mosaic_maxzoom"] == 9 + assert body["properties"]["mosaic_tilematrixset"] def test_tilejson(app): - """test GET /mosaicjson/tilejson.json endpoint""" + """test GET /mosaicjson/WebMercatorQuad/tilejson.json endpoint""" mosaicjson = read_json_fixture(MOSAICJSON_FILE) - response = app.get("/mosaicjson/tilejson.json", params={"url": MOSAICJSON_FILE}) + response = app.get( + "/mosaicjson/WebMercatorQuad/tilejson.json", params={"url": MOSAICJSON_FILE} + ) assert response.status_code == 200 body = response.json() TileJSON(**body) @@ -111,13 +115,14 @@ def test_tile(app): """Test GET /mosaicjson/tiles endpoint""" mosaicjson = read_json_fixture(MOSAICJSON_FILE) bounds = mosaicjson["bounds"] - tile = mercantile.tile(*mosaicjson["center"]) - partial_tile = mercantile.tile(bounds[0], bounds[1], mosaicjson["minzoom"]) + tms = morecantile.tms.get("WebMercatorQuad") + tile = tms.tile(*mosaicjson["center"]) + partial_tile = tms.tile(bounds[0], bounds[1], mosaicjson["minzoom"]) with patch.object(FileBackend, "_read", mosaic_read_factory(MOSAICJSON_FILE)): # full tile response = app.get( - f"/mosaicjson/tiles/{tile.z}/{tile.x}/{tile.y}", + f"/mosaicjson/tiles/WebMercatorQuad/{tile.z}/{tile.x}/{tile.y}", params={"url": MOSAICJSON_FILE}, ) assert response.status_code == 200 @@ -126,7 +131,7 @@ def test_tile(app): assert meta["width"] == meta["height"] == 256 response = app.get( - f"/mosaicjson/tiles/{tile.z}/{tile.x}/{tile.y}@2x", + f"/mosaicjson/tiles/WebMercatorQuad/{tile.z}/{tile.x}/{tile.y}@2x", params={"url": MOSAICJSON_FILE}, ) assert response.status_code == 200 @@ -135,7 +140,7 @@ def test_tile(app): assert meta["width"] == meta["height"] == 512 response = app.get( - f"/mosaicjson/tiles/{tile.z}/{tile.x}/{tile.y}.tif", + f"/mosaicjson/tiles/WebMercatorQuad/{tile.z}/{tile.x}/{tile.y}.tif", params={"url": MOSAICJSON_FILE}, ) assert response.status_code == 200 @@ -145,7 +150,7 @@ def test_tile(app): assert meta["crs"] == 3857 response = app.get( - f"/mosaicjson/tiles/{tile.z}/{tile.x}/{tile.y}@2x.tif", + f"/mosaicjson/tiles/WebMercatorQuad/{tile.z}/{tile.x}/{tile.y}@2x.tif", params={"url": MOSAICJSON_FILE, "nodata": 0, "bidx": 1}, ) assert response.status_code == 200 @@ -157,7 +162,7 @@ def test_tile(app): assert meta["height"] == 512 response = app.get( - f"/mosaicjson/tiles/{tile.z}/{tile.x}/{tile.y}@2x.jpg", + f"/mosaicjson/tiles/WebMercatorQuad/{tile.z}/{tile.x}/{tile.y}@2x.jpg", params={ "url": MOSAICJSON_FILE, "rescale": "0,1000", @@ -170,14 +175,14 @@ def test_tile(app): # partial tile response = app.get( - f"/mosaicjson/tiles/{partial_tile.z}/{partial_tile.x}/{partial_tile.y}", + f"/mosaicjson/tiles/WebMercatorQuad/{partial_tile.z}/{partial_tile.x}/{partial_tile.y}", params={"url": MOSAICJSON_FILE}, ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" response = app.get( - f"/mosaicjson/tiles/{partial_tile.z}/{partial_tile.x}/{partial_tile.y}.tif", + f"/mosaicjson/tiles/WebMercatorQuad/{partial_tile.z}/{partial_tile.x}/{partial_tile.y}.tif", params={"url": MOSAICJSON_FILE, "resampling": "bilinear"}, ) assert response.status_code == 200 @@ -185,10 +190,11 @@ def test_tile(app): def test_wmts(app): - """test GET /mosaicjson/WMTSCapabilities.xml endpoint""" + """test GET /mosaicjson/WebMercatorQuad/WMTSCapabilities.xml endpoint""" with patch.object(FileBackend, "_read", mosaic_read_factory(MOSAICJSON_FILE)): response = app.get( - "/mosaicjson/WMTSCapabilities.xml", params={"url": MOSAICJSON_FILE} + "/mosaicjson/WebMercatorQuad/WMTSCapabilities.xml", + params={"url": MOSAICJSON_FILE}, ) assert response.status_code == 200 assert response.headers["content-type"] == "application/xml" @@ -198,7 +204,7 @@ def test_wmts(app): ) response = app.get( - "/mosaicjson/WMTSCapabilities.xml", + "/mosaicjson/WebMercatorQuad/WMTSCapabilities.xml", params={"url": MOSAICJSON_FILE, "tile_scale": 2}, ) assert response.status_code == 200 diff --git a/src/titiler/application/tests/routes/test_stac.py b/src/titiler/application/tests/routes/test_stac.py index 2ced6f0dd..d4220a10d 100644 --- a/src/titiler/application/tests/routes/test_stac.py +++ b/src/titiler/application/tests/routes/test_stac.py @@ -1,9 +1,9 @@ -"""test /COG endpoints.""" - +"""test /stac endpoints.""" from typing import Dict from unittest.mock import patch +import pytest from rasterio.io import MemoryFile from ..conftest import mock_rasterio_open, mock_RequestGet @@ -37,7 +37,9 @@ def test_info(httpx, rio, app): body = response.json() assert body["B01"] - response = app.get("/stac/info?url=https://myurl.com/item.json") + # no assets + with pytest.warns(UserWarning): + response = app.get("/stac/info?url=https://myurl.com/item.json") assert response.status_code == 200 body = response.json() assert body["B01"] @@ -75,11 +77,13 @@ def test_tile(httpx, rio, app): rio.open = mock_rasterio_open # Missing assets - response = app.get("/stac/tiles/9/289/207?url=https://myurl.com/item.json") + response = app.get( + "/stac/tiles/WebMercatorQuad/9/289/207?url=https://myurl.com/item.json" + ) assert response.status_code == 400 response = app.get( - "/stac/tiles/9/289/207?url=https://myurl.com/item.json&assets=B01&rescale=0,1000" + "/stac/tiles/WebMercatorQuad/9/289/207?url=https://myurl.com/item.json&assets=B01&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" @@ -88,7 +92,7 @@ def test_tile(httpx, rio, app): assert meta["height"] == 256 response = app.get( - "/stac/tiles/9/289/207?url=https://myurl.com/item.json&expression=B01_b1&rescale=0,1000" + "/stac/tiles/WebMercatorQuad/9/289/207?url=https://myurl.com/item.json&expression=B01_b1&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" @@ -104,11 +108,13 @@ def test_tilejson(httpx, rio, app): httpx.get = mock_RequestGet rio.open = mock_rasterio_open - response = app.get("/stac/tilejson.json?url=https://myurl.com/item.json") + response = app.get( + "/stac/WebMercatorQuad/tilejson.json?url=https://myurl.com/item.json" + ) assert response.status_code == 400 response = app.get( - "/stac/tilejson.json?url=https://myurl.com/item.json&assets=B01&minzoom=5&maxzoom=10" + "/stac/WebMercatorQuad/tilejson.json?url=https://myurl.com/item.json&assets=B01&minzoom=5&maxzoom=10" ) assert response.status_code == 200 body = response.json() @@ -125,7 +131,7 @@ def test_tilejson(httpx, rio, app): assert body["center"] response = app.get( - "/stac/tilejson.json?url=https://myurl.com/item.json&assets=B01&tile_format=png&tile_scale=2" + "/stac/WebMercatorQuad/tilejson.json?url=https://myurl.com/item.json&assets=B01&tile_format=png&tile_scale=2" ) assert response.status_code == 200 body = response.json() @@ -182,12 +188,12 @@ def test_part(httpx, rio, app): # Missing Assets or Expression response = app.get( - "/stac/crop/23.878,32.063,23.966,32.145.png?url=https://myurl.com/item.json" + "/stac/bbox/23.878,32.063,23.966,32.145.png?url=https://myurl.com/item.json" ) assert response.status_code == 400 response = app.get( - "/stac/crop/23.878,32.063,23.966,32.145.png?url=https://myurl.com/item.json&assets=B01&rescale=0,1000&max_size=64" + "/stac/bbox/23.878,32.063,23.966,32.145.png?url=https://myurl.com/item.json&assets=B01&rescale=0,1000&max_size=64" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" @@ -196,7 +202,7 @@ def test_part(httpx, rio, app): assert meta["height"] == 14 response = app.get( - "/stac/crop/23.878,32.063,23.966,32.145.png?url=https://myurl.com/item.json&assets=B01&rescale=0,1000&max_size=64&width=128&height=128" + "/stac/bbox/23.878,32.063,23.966,32.145.png?url=https://myurl.com/item.json&assets=B01&rescale=0,1000&max_size=64&width=128&height=128" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" @@ -205,7 +211,7 @@ def test_part(httpx, rio, app): assert meta["height"] == 128 response = app.get( - "/stac/crop/23.878,32.063,23.966,32.145.png?url=https://myurl.com/item.json&expression=B01_b1&rescale=0,1000&max_size=64" + "/stac/bbox/23.878,32.063,23.966,32.145.png?url=https://myurl.com/item.json&expression=B01_b1&rescale=0,1000&max_size=64" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" @@ -217,7 +223,7 @@ def test_part(httpx, rio, app): @patch("rio_tiler.io.rasterio.rasterio") @patch("rio_tiler.io.stac.httpx") def test_point(httpx, rio, app): - """test crop endpoints.""" + """test point endpoints.""" httpx.get = mock_RequestGet rio.open = mock_rasterio_open diff --git a/src/titiler/application/tests/test_main.py b/src/titiler/application/tests/test_main.py index 81ddba727..230b8473d 100644 --- a/src/titiler/application/tests/test_main.py +++ b/src/titiler/application/tests/test_main.py @@ -5,4 +5,17 @@ def test_health(app): """Test /healthz endpoint.""" response = app.get("/healthz") assert response.status_code == 200 - assert response.json() == {"ping": "pong!"} + resp = response.json() + assert set(resp["versions"].keys()) == { + "titiler", + "gdal", + "geos", + "proj", + "rasterio", + } + + response = app.get("/api") + assert response.status_code == 200 + + response = app.get("/api.html") + assert response.status_code == 200 diff --git a/src/titiler/application/titiler/application/__init__.py b/src/titiler/application/titiler/application/__init__.py index f59030372..43ff64612 100644 --- a/src/titiler/application/titiler/application/__init__.py +++ b/src/titiler/application/titiler/application/__init__.py @@ -1,3 +1,3 @@ """titiler.application""" -__version__ = "0.11.7" +__version__ = "0.22.4" diff --git a/src/titiler/application/titiler/application/main.py b/src/titiler/application/titiler/application/main.py index d6366d8bd..34665e5e6 100644 --- a/src/titiler/application/titiler/application/main.py +++ b/src/titiler/application/titiler/application/main.py @@ -1,14 +1,16 @@ """titiler app.""" +import json import logging +from logging import config as log_config +from typing import Annotated, Literal, Optional -import jinja2 -from fastapi import FastAPI -from rio_tiler.io import STACReader +import rasterio +from fastapi import Depends, FastAPI, HTTPException, Query, Security +from fastapi.security.api_key import APIKeyQuery +from rio_tiler.io import Reader, STACReader from starlette.middleware.cors import CORSMiddleware from starlette.requests import Request -from starlette.responses import HTMLResponse -from starlette.templating import Jinja2Templates from starlette_cramjam.middleware import CompressionMiddleware from titiler.application import __version__ as titiler_version @@ -16,6 +18,7 @@ from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers from titiler.core.factory import ( AlgorithmFactory, + ColorMapFactory, MultiBaseTilerFactory, TilerFactory, TMSFactory, @@ -26,35 +29,57 @@ LowerCaseQueryStringMiddleware, TotalTimeMiddleware, ) +from titiler.core.models.OGC import Conformance, Landing +from titiler.core.resources.enums import MediaType +from titiler.core.templating import create_html_response +from titiler.core.utils import accept_media_type, update_openapi from titiler.extensions import ( cogValidateExtension, cogViewerExtension, stacExtension, + stacRenderExtension, stacViewerExtension, ) from titiler.mosaic.errors import MOSAIC_STATUS_CODES from titiler.mosaic.factory import MosaicTilerFactory -try: - pass # type: ignore -except ImportError: - # Try backported to PY<39 `importlib_resources`. - pass # type: ignore - logging.getLogger("botocore.credentials").disabled = True logging.getLogger("botocore.utils").disabled = True +logging.getLogger("rasterio.session").setLevel(logging.ERROR) logging.getLogger("rio-tiler").setLevel(logging.ERROR) -templates = Jinja2Templates( - directory="", - loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]), -) # type:ignore - api_settings = ApiSettings() +app_dependencies = [] +if api_settings.global_access_token: + ############################################################################### + # Setup a global API access key, if configured + api_key_query = APIKeyQuery(name="access_token", auto_error=False) + + def validate_access_token(access_token: str = Security(api_key_query)): + """Validates API key access token, set as the `api_settings.global_access_token` value. + Returns True if no access token is required, or if the access token is valid. + Raises an HTTPException (401) if the access token is required but invalid/missing. + """ + if not access_token: + raise HTTPException(status_code=401, detail="Missing `access_token`") + + # if access_token == `token` then OK + if access_token != api_settings.global_access_token: + raise HTTPException(status_code=401, detail="Invalid `access_token`") + + return True + + app_dependencies.append(Depends(validate_access_token)) + + +############################################################################### + app = FastAPI( title=api_settings.name, + openapi_url="/api", + docs_url="/api.html", description="""A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL. --- @@ -67,22 +92,42 @@ """, version=titiler_version, root_path=api_settings.root_path, + dependencies=app_dependencies, ) +# Fix OpenAPI response header for OGC Common compatibility +update_openapi(app) + +TITILER_CONFORMS_TO = { + "http://www.opengis.net/spec/ogcapi-common-1/1.0/req/core", + "http://www.opengis.net/spec/ogcapi-common-1/1.0/req/landing-page", + "http://www.opengis.net/spec/ogcapi-common-1/1.0/req/oas30", + "http://www.opengis.net/spec/ogcapi-common-1/1.0/req/html", + "http://www.opengis.net/spec/ogcapi-common-1/1.0/req/json", +} + + ############################################################################### # Simple Dataset endpoints (e.g Cloud Optimized GeoTIFF) if not api_settings.disable_cog: cog = TilerFactory( + reader=Reader, router_prefix="/cog", extensions=[ cogValidateExtension(), cogViewerExtension(), stacExtension(), ], + enable_telemetry=api_settings.telemetry_enabled, ) - app.include_router(cog.router, prefix="/cog", tags=["Cloud Optimized GeoTIFF"]) + app.include_router( + cog.router, + prefix="/cog", + tags=["Cloud Optimized GeoTIFF"], + ) + TITILER_CONFORMS_TO.update(cog.conforms_to) ############################################################################### # STAC endpoints @@ -92,28 +137,61 @@ router_prefix="/stac", extensions=[ stacViewerExtension(), + stacRenderExtension(), ], + enable_telemetry=api_settings.telemetry_enabled, ) app.include_router( - stac.router, prefix="/stac", tags=["SpatioTemporal Asset Catalog"] + stac.router, + prefix="/stac", + tags=["SpatioTemporal Asset Catalog"], ) + TITILER_CONFORMS_TO.update(stac.conforms_to) + ############################################################################### # Mosaic endpoints if not api_settings.disable_mosaic: - mosaic = MosaicTilerFactory(router_prefix="/mosaicjson") - app.include_router(mosaic.router, prefix="/mosaicjson", tags=["MosaicJSON"]) + mosaic = MosaicTilerFactory( + router_prefix="/mosaicjson", + enable_telemetry=api_settings.telemetry_enabled, + ) + app.include_router( + mosaic.router, + prefix="/mosaicjson", + tags=["MosaicJSON"], + ) + + TITILER_CONFORMS_TO.update(mosaic.conforms_to) ############################################################################### # TileMatrixSets endpoints tms = TMSFactory() -app.include_router(tms.router, tags=["Tiling Schemes"]) +app.include_router( + tms.router, + tags=["Tiling Schemes"], +) +TITILER_CONFORMS_TO.update(tms.conforms_to) ############################################################################### # Algorithms endpoints algorithms = AlgorithmFactory() -app.include_router(algorithms.router, tags=["Algorithms"]) +app.include_router( + algorithms.router, + tags=["Algorithms"], +) +TITILER_CONFORMS_TO.update(algorithms.conforms_to) + +############################################################################### +# Colormaps endpoints +cmaps = ColorMapFactory() +app.include_router( + cmaps.router, + tags=["ColorMaps"], +) +TITILER_CONFORMS_TO.update(cmaps.conforms_to) + add_exception_handlers(app, DEFAULT_STATUS_CODES) add_exception_handlers(app, MOSAIC_STATUS_CODES) @@ -124,7 +202,7 @@ CORSMiddleware, allow_origins=api_settings.cors_origins, allow_credentials=True, - allow_methods=["GET"], + allow_methods=api_settings.cors_allow_methods, allow_headers=["*"], ) @@ -138,6 +216,7 @@ "image/jp2", "image/webp", }, + compression_level=6, ) app.add_middleware( @@ -147,9 +226,70 @@ ) if api_settings.debug: - app.add_middleware(LoggerMiddleware, headers=True, querystrings=True) + app.add_middleware(LoggerMiddleware) app.add_middleware(TotalTimeMiddleware) + log_config.dictConfig( + { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "detailed": { + "format": "%(asctime)s - %(levelname)s - %(name)s - %(message)s" + }, + "request": { + "format": ( + "%(asctime)s - %(levelname)s - %(name)s - %(message)s " + + json.dumps( + { + k: f"%({k})s" + for k in [ + "http.method", + "http.referer", + "http.request.header.origin", + "http.route", + "http.target", + "http.request.header.content-length", + "http.request.header.accept-encoding", + "http.request.header.origin", + "titiler.path_params", + "titiler.query_params", + ] + } + ) + ), + }, + }, + "handlers": { + "console_detailed": { + "class": "logging.StreamHandler", + "level": "WARNING", + "formatter": "detailed", + "stream": "ext://sys.stdout", + }, + "console_request": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "request", + "stream": "ext://sys.stdout", + }, + }, + "loggers": { + "titiler": { + "level": "INFO", + "handlers": ["console_detailed"], + "propagate": False, + }, + "titiler.requests": { + "level": "INFO", + "handlers": ["console_request"], + "propagate": False, + }, + }, + } + ) + + if api_settings.lower_case_query_parameters: app.add_middleware(LowerCaseQueryStringMiddleware) @@ -161,16 +301,188 @@ operation_id="healthCheck", tags=["Health Check"], ) -def ping(): +def application_health_check(): """Health check.""" - return {"ping": "pong!"} + return { + "versions": { + "titiler": titiler_version, + "rasterio": rasterio.__version__, + "gdal": rasterio.__gdal_version__, + "proj": rasterio.__proj_version__, + "geos": rasterio.__geos_version__, + } + } + + +@app.get( + "/", + response_model=Landing, + response_model_exclude_none=True, + responses={ + 200: { + "content": { + "text/html": {}, + "application/json": {}, + } + }, + }, + tags=["OGC Common"], +) +def landing( + request: Request, + f: Annotated[ + Optional[Literal["html", "json"]], + Query( + description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header." + ), + ] = None, +): + """TiTiler landing page.""" + data = { + "title": "TiTiler", + "description": "A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL.", + "links": [ + { + "title": "Landing page", + "href": str(request.url_for("landing")), + "type": "text/html", + "rel": "self", + }, + { + "title": "The API definition (JSON)", + "href": str(request.url_for("openapi")), + "type": "application/vnd.oai.openapi+json;version=3.0", + "rel": "service-desc", + }, + { + "title": "The API documentation", + "href": str(request.url_for("swagger_ui_html")), + "type": "text/html", + "rel": "service-doc", + }, + { + "title": "Conformance Declaration", + "href": str(request.url_for("conformance")), + "type": "text/html", + "rel": "http://www.opengis.net/def/rel/ogc/1.0/conformance", + }, + { + "title": "List of Available TileMatrixSets", + "href": str(request.url_for("tilematrixsets")), + "type": "application/json", + "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-schemes", + }, + { + "title": "TiTiler Documentation (external link)", + "href": "https://developmentseed.org/titiler/", + "type": "text/html", + "rel": "doc", + }, + { + "title": "TiTiler source code (external link)", + "href": "https://github.com/developmentseed/titiler", + "type": "text/html", + "rel": "doc", + }, + ], + } + + output_type: Optional[MediaType] + if f: + output_type = MediaType[f] + else: + accepted_media = [MediaType.html, MediaType.json] + output_type = accept_media_type( + request.headers.get("accept", ""), accepted_media + ) + + if output_type == MediaType.html: + return create_html_response( + request, + data, + title="TiTiler", + template_name="landing", + ) + + return data -@app.get("/", response_class=HTMLResponse, include_in_schema=False) -def landing(request: Request): - """TiTiler Landing page""" - return templates.TemplateResponse( - name="index.html", - context={"request": request}, - media_type="text/html", +@app.get( + "/conformance", + response_model=Conformance, + response_model_exclude_none=True, + responses={ + 200: { + "content": { + "text/html": {}, + "application/json": {}, + } + }, + }, + tags=["OGC Common"], +) +def conformance( + request: Request, + f: Annotated[ + Optional[Literal["html", "json"]], + Query( + description="Response MediaType. Defaults to endpoint's default or value defined in `accept` header." + ), + ] = None, +): + """Conformance classes. + + Called with `GET /conformance`. + + Returns: + Conformance classes which the server conforms to. + + """ + data = {"conformsTo": sorted(TITILER_CONFORMS_TO)} + + output_type: Optional[MediaType] + if f: + output_type = MediaType[f] + else: + accepted_media = [MediaType.html, MediaType.json] + output_type = accept_media_type( + request.headers.get("accept", ""), accepted_media + ) + + if output_type == MediaType.html: + return create_html_response( + request, + data, + title="Conformance", + template_name="conformance", + ) + + return data + + +if api_settings.telemetry_enabled: + from opentelemetry import trace + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter + from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor + from opentelemetry.instrumentation.logging import LoggingInstrumentor + from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_VERSION, Resource + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor + + LoggingInstrumentor().instrument(set_logging_format=True) + FastAPIInstrumentor.instrument_app(app) + + resource = Resource.create( + { + SERVICE_NAME: "titiler", + SERVICE_VERSION: titiler_version, + } ) + + provider = TracerProvider(resource=resource) + + # uses the OTEL_EXPORTER_OTLP_ENDPOINT env var + processor = BatchSpanProcessor(OTLPSpanExporter()) + provider.add_span_processor(processor) + + trace.set_tracer_provider(provider) diff --git a/src/titiler/application/titiler/application/settings.py b/src/titiler/application/titiler/application/settings.py index 09fe63175..70c45dd3b 100644 --- a/src/titiler/application/titiler/application/settings.py +++ b/src/titiler/application/titiler/application/settings.py @@ -1,13 +1,17 @@ """Titiler API settings.""" -import pydantic +from typing import Optional +from pydantic import field_validator +from pydantic_settings import BaseSettings, SettingsConfigDict -class ApiSettings(pydantic.BaseSettings): + +class ApiSettings(BaseSettings): """FASTAPI application settings.""" name: str = "TiTiler" cors_origins: str = "*" + cors_allow_methods: str = "GET" cachecontrol: str = "public, max-age=3600" root_path: str = "" debug: bool = False @@ -18,13 +22,21 @@ class ApiSettings(pydantic.BaseSettings): lower_case_query_parameters: bool = False - @pydantic.validator("cors_origins") + telemetry_enabled: bool = True + + # an API key required to access any endpoint, passed via the ?access_token= query parameter + global_access_token: Optional[str] = None + + model_config = SettingsConfigDict( + env_prefix="TITILER_API_", env_file=".env", extra="ignore" + ) + + @field_validator("cors_origins") def parse_cors_origin(cls, v): """Parse CORS origins.""" return [origin.strip() for origin in v.split(",")] - class Config: - """model config""" - - env_file = ".env" - env_prefix = "TITILER_API_" + @field_validator("cors_allow_methods") + def parse_cors_allow_methods(cls, v): + """Parse CORS allowed methods.""" + return [method.strip().upper() for method in v.split(",")] diff --git a/src/titiler/application/titiler/application/templates/index.html b/src/titiler/application/titiler/application/templates/index.html deleted file mode 100644 index 70f9c7ea3..000000000 --- a/src/titiler/application/titiler/application/templates/index.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - TiTiler - - - - -
-
- ______   __     ______   __     __         ______     ______
-/\__  _\ /\ \   /\__  _\ /\ \   /\ \       /\  ___\   /\  == \
-\/_/\ \/ \ \ \  \/_/\ \/ \ \ \  \ \ \____  \ \  __\   \ \  __<
-   \ \_\  \ \_\    \ \_\  \ \_\  \ \_____\  \ \_____\  \ \_\ \_\
-    \/_/   \/_/     \/_/   \/_/   \/_____/   \/_____/   \/_/ /_/
-            
- -

API documentations: /docs -

-

TiTiler Online documentations: https://developmentseed.org/titiler/ -

-

- -
- Created by - - Development Seed - -
- - diff --git a/src/titiler/core/LICENSE b/src/titiler/core/LICENSE new file mode 100644 index 000000000..eb4365951 --- /dev/null +++ b/src/titiler/core/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Development Seed + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/titiler/core/README.md b/src/titiler/core/README.md index 4a8fa87c1..ae5804fa7 100644 --- a/src/titiler/core/README.md +++ b/src/titiler/core/README.md @@ -5,14 +5,14 @@ Core of Titiler's application. Contains blocks to create dynamic tile servers. ## Installation ```bash -$ pip install -U pip +python -m pip install -U pip # From Pypi -$ pip install titiler.core +python -m pip install titiler.core # Or from sources -$ git clone https://github.com/developmentseed/titiler.git -$ cd titiler && pip install -e titiler/core +git clone https://github.com/developmentseed/titiler.git +cd titiler && python -m pip install -e src/titiler/core ``` ## How To @@ -33,8 +33,6 @@ cog = TilerFactory() app.include_router(cog.router, tags=["Cloud Optimized GeoTIFF"]) ``` -See [titiler.application](../application) for a full example. - ## Package structure ``` @@ -42,6 +40,10 @@ titiler/ └── core/ ├── tests/ - Tests suite └── titiler/core/ - `core` namespace package + ├── algorithm/ + | ├── base.py - ABC Base Class for custom algorithms + | ├── dem.py - Elevation data related algorithms + | └── index.py - Simple band index algorithms ├── models/ | ├── response.py - Titiler's response models | ├── mapbox.py - Mapbox TileJSON pydantic model @@ -50,10 +52,13 @@ titiler/ | ├── enums.py - Titiler's enumerations (e.g MediaType) | └── responses.py - Custom Starlette's responses ├── templates/ - | └── wmts.xml - OGC WMTS template + | ├── map.html - Simple Map viewer (built with leaflet) + | └── wmts.xml - OGC WMTS document template ├── dependencies.py - Titiler FastAPI's dependencies ├── errors.py - Errors handler factory + ├── middleware.py - Starlette middlewares ├── factory.py - Dynamic tiler endpoints factories ├── routing.py - Custom APIRoute class + ├── telemetry.py - OpenTelemetry tracing functions └── utils.py - Titiler utility functions ``` diff --git a/src/titiler/core/pyproject.toml b/src/titiler/core/pyproject.toml index f964c014a..bd775ef9c 100644 --- a/src/titiler/core/pyproject.toml +++ b/src/titiler/core/pyproject.toml @@ -1,8 +1,8 @@ [project] -name = "titiler.core" +name = "titiler-core" description = "A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" authors = [ {name = "Vincent Sarago", email = "vincent@developmentseed.com"}, ] @@ -21,23 +21,26 @@ classifiers = [ "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: GIS", ] dynamic = ["version"] dependencies = [ - "fastapi>=0.87.0,<0.95", - "geojson-pydantic", + "fastapi>=0.108.0", + "geojson-pydantic>=1.1.2,<3.0", "jinja2>=2.11.2,<4.0.0", "numpy", - "pydantic", + "pydantic~=2.0", "rasterio", - "rio-tiler>=4.1.6,<4.2", + "rio-tiler>=7.7,<8.0", + "morecantile", "simplejson", - "typing_extensions;python_version<'3.8'", + "typing_extensions>=4.6.1", ] [project.optional-dependencies] @@ -47,6 +50,13 @@ test = [ "pytest-asyncio", "httpx", ] +telemetry = [ + "opentelemetry-api", + "opentelemetry-sdk", + "opentelemetry-instrumentation-fastapi", + "opentelemetry-instrumentation-logging", + "opentelemetry-exporter-otlp", +] [project.urls] Homepage = "https://developmentseed.org/titiler/" @@ -56,8 +66,8 @@ Source = "https://github.com/developmentseed/titiler" Changelog = "https://developmentseed.org/titiler/release-notes/" [build-system] -requires = ["pdm-pep517"] -build-backend = "pdm.pep517.api" +requires = ["pdm-backend"] +build-backend = "pdm.backend" [tool.pdm.version] source = "file" diff --git a/src/titiler/core/tests/fixtures/cog_dateline.tif b/src/titiler/core/tests/fixtures/cog_dateline.tif new file mode 100644 index 000000000..279183018 Binary files /dev/null and b/src/titiler/core/tests/fixtures/cog_dateline.tif differ diff --git a/src/titiler/core/tests/fixtures/item.json b/src/titiler/core/tests/fixtures/item.json index f8e447265..8a20a9ed5 100644 --- a/src/titiler/core/tests/fixtures/item.json +++ b/src/titiler/core/tests/fixtures/item.json @@ -1,18 +1,10 @@ { "type": "Feature", - "stac_version": "1.0.0-beta.1", - "stac_extensions": [ - "eo", - "view", - "proj" - ], + "stac_version": "1.0.0", "id": "S2A_34SGA_20200318_0_L2A", - "bbox": [ - 23.293255090449595, - 31.505183020453355, - 24.296453548295318, - 32.51147809805106 - ], + "properties": { + "datetime": "2020-03-18T09:11:33Z" + }, "geometry": { "type": "Polygon", "coordinates": [ @@ -40,91 +32,31 @@ ] ] }, - "properties": { - "datetime": "2020-03-18T09:11:33Z", - "platform": "sentinel-2a", - "constellation": "sentinel-2", - "instruments": [ - "msi" - ], - "gsd": 10, - "data_coverage": 73.85, - "view:off_nadir": 0, - "eo:cloud_cover": 89.84, - "proj:epsg": 32634, - "sentinel:latitude_band": "S", - "sentinel:grid_square": "GA", - "sentinel:sequence": "0", - "sentinel:product_id": "S2A_MSIL2A_20200318T085701_N0214_R007_T34SGA_20200318T115254", - "created": "2020-05-12T21:03:26.671Z", - "updated": "2020-05-12T21:03:26.671Z" - }, - "collection": "sentinel-s2-l2a-cogs", + "links": [ + { + "rel": "self", + "href": "https://myurl.com/item.json", + "type": "application/json" + } + ], "assets": { "B01": { - "title": "Band 1 (coastal)", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", "href": "https://myurl.com/B01.tif", - "proj:shape": [ - 1830, - 1830 - ], - "proj:transform": [ - 60, - 0, - 699960, - 0, - -60, - 3600000, - 0, - 0, - 1 - ] + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Band 1 (coastal)" }, "B09": { - "title": "Band 9", - "type": "image/tiff; application=geotiff; profile=cloud-optimized", "href": "https://myurl.com/B09.tif", - "proj:shape": [ - 1830, - 1830 - ], - "proj:transform": [ - 60, - 0, - 699960, - 0, - -60, - 3600000, - 0, - 0, - 1 - ] + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "title": "Band 9" } }, - "links": [ - { - "rel": "self", - "href": "s3://sentinel-cogs/sentinel-s2-l2a-cogs/2020/S2A_34SGA_20200318_0_L2A/S2A_34SGA_20200318_0_L2A.json", - "type": "application/json" - }, - { - "rel": "parent", - "href": "https://myurl.com/v0/collections/sentinel-s2-l2a" - }, - { - "rel": "collection", - "href": "https://myurl.com/v0/collections/sentinel-s2-l2a" - }, - { - "rel": "root", - "href": "https://myurl.com/v0/" - }, - { - "title": "Source STAC Item", - "rel": "derived_from", - "href": "https://myurl.com/collections/sentinel-s2-l2a/items/S2A_34SGA_20200318_0_L2A", - "type": "application/json" - } - ] + "bbox": [ + 23.293255090449595, + 31.505183020453355, + 24.296453548295318, + 32.51147809805106 + ], + "stac_extensions": [], + "collection": "sentinel-s2-l2a-cogs" } diff --git a/src/titiler/core/tests/test_CustomCmap.py b/src/titiler/core/tests/test_CustomCmap.py index 0c189247a..8b396aaf7 100644 --- a/src/titiler/core/tests/test_CustomCmap.py +++ b/src/titiler/core/tests/test_CustomCmap.py @@ -1,14 +1,13 @@ """Test TiTiler Custom Colormap Params.""" -from enum import Enum from io import BytesIO -from typing import Dict, Optional import numpy -from fastapi import FastAPI, Query +from fastapi import FastAPI from rio_tiler.colormap import ColorMaps from starlette.testclient import TestClient +from titiler.core.dependencies import create_colormap_dependency from titiler.core.factory import TilerFactory from .conftest import DATA_DIR @@ -17,19 +16,8 @@ "cmap1": {6: (4, 5, 6, 255)}, } cmap = ColorMaps(data=cmap_values) -ColorMapName = Enum( # type: ignore - "ColorMapName", [(a, a) for a in sorted(cmap.list())] -) - -def ColorMapParams( - colormap_name: ColorMapName = Query(None, description="Colormap name"), -) -> Optional[Dict]: - """Colormap Dependency.""" - if colormap_name: - return cmap.get(colormap_name.value) - - return None +ColorMapParams = create_colormap_dependency(cmap) def test_CustomCmap(): diff --git a/src/titiler/core/tests/test_CustomPath.py b/src/titiler/core/tests/test_CustomPath.py index cb1bd3298..dd308d630 100644 --- a/src/titiler/core/tests/test_CustomPath.py +++ b/src/titiler/core/tests/test_CustomPath.py @@ -5,6 +5,7 @@ from fastapi import FastAPI, HTTPException, Query from starlette.testclient import TestClient +from typing_extensions import Annotated from titiler.core.factory import TilerFactory @@ -12,11 +13,13 @@ def CustomPathParams( - name: str = Query( - ..., - alias="file", - description="Give me a url.", - ) + name: Annotated[ + str, + Query( + alias="file", + description="Give me a url.", + ), + ], ) -> str: """Custom path Dependency.""" if not re.match(".+tif$", name): diff --git a/src/titiler/core/tests/test_CustomRender.py b/src/titiler/core/tests/test_CustomRender.py index 2d9661e2a..d24408f8e 100644 --- a/src/titiler/core/tests/test_CustomRender.py +++ b/src/titiler/core/tests/test_CustomRender.py @@ -30,6 +30,7 @@ class CustomRenderParams(ImageRenderingParams): def __post_init__(self): """post init.""" + super().__post_init__() if self.nodata is not None: self.nodata = numpy.nan if self.nodata == "nan" else float(self.nodata) @@ -41,7 +42,7 @@ def test_CustomRender(): app.include_router(cog.router) client = TestClient(app) - response = client.get(f"/tiles/8/87/48.tif?url={DATA_DIR}/cog.tif") + response = client.get(f"/tiles/WebMercatorQuad/8/87/48.tif?url={DATA_DIR}/cog.tif") assert response.status_code == 200 assert response.headers["content-type"] == "image/tiff; application=geotiff" meta = parse_img(response.content) @@ -51,7 +52,7 @@ def test_CustomRender(): assert not meta.get("compress") response = client.get( - f"/tiles/8/87/48.tif?url={DATA_DIR}/cog.tif&return_mask=false&output_nodata=0&output_compression=deflate" + f"/tiles/WebMercatorQuad/8/87/48.tif?url={DATA_DIR}/cog.tif&return_mask=false&output_nodata=0&output_compression=deflate" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/tiff; application=geotiff" @@ -62,7 +63,7 @@ def test_CustomRender(): assert meta["compress"] == "deflate" response = client.get( - f"/tiles/9/289/207?url={DATA_DIR}/TCI.tif&rescale=0,1000&rescale=0,2000&rescale=0,3000" + f"/tiles/WebMercatorQuad/9/289/207?url={DATA_DIR}/TCI.tif&rescale=0,1000&rescale=0,2000&rescale=0,3000" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" diff --git a/src/titiler/core/tests/test_algorithms.py b/src/titiler/core/tests/test_algorithms.py index ca4e95a59..c48f404b6 100644 --- a/src/titiler/core/tests/test_algorithms.py +++ b/src/titiler/core/tests/test_algorithms.py @@ -3,6 +3,7 @@ import json import numpy +import pytest from fastapi import Depends, FastAPI from rasterio.io import MemoryFile from rio_tiler.models import ImageData @@ -22,12 +23,11 @@ class Multiply(BaseAlgorithm): def __call__(self, img: ImageData) -> ImageData: """Apply Multiplication factor.""" # Multiply image data bcy factor - data = img.data * self.factor + data = img.array * self.factor # Create output ImageData return ImageData( data, - img.mask, assets=img.assets, crs=img.crs, bounds=img.bounds, @@ -88,7 +88,6 @@ def main(algorithm=Depends(default_algorithms.dependency)): # MAPBOX Terrain RGB response = client.get("/", params={"algorithm": "terrainrgb"}) assert response.status_code == 200 - with MemoryFile(response.content) as mem: with mem.open() as dst: data = dst.read().astype(numpy.float64) @@ -100,7 +99,6 @@ def main(algorithm=Depends(default_algorithms.dependency)): # TILEZEN Terrarium response = client.get("/", params={"algorithm": "terrarium"}) assert response.status_code == 200 - with MemoryFile(response.content) as mem: with mem.open() as dst: data = dst.read().astype(numpy.float64) @@ -108,3 +106,246 @@ def main(algorithm=Depends(default_algorithms.dependency)): # https://github.com/tilezen/joerd/blob/master/docs/formats.md#terrarium elevation = (data[0] * 256 + data[1] + data[2] / 256) - 32768 numpy.testing.assert_array_equal(elevation, arr[0]) + + +def test_normalized_index(): + """test ndi.""" + algo = default_algorithms.get("normalizedIndex")() + + arr = numpy.zeros((2, 256, 256), dtype="uint16") + arr[0, :, :] = 1 + arr[1, :, :] = 2 + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "float32" + assert len(numpy.unique(out.array).tolist()) == 1 + numpy.testing.assert_almost_equal(out.array[0, 0, 0], 0.3333, decimal=3) + + # with mixed 0 and masked + arr = numpy.ma.MaskedArray( + numpy.zeros((2, 256, 256), dtype="uint16"), + mask=numpy.zeros((2, 256, 256), dtype="bool"), + ) + arr.data[0, :, :] = 1 + arr.data[0, 0:10, 0:10] = 0 + arr.mask[0, 0:5, 0:5] = True + + arr.data[1, :, :] = 2 + arr.data[1, 0:10, 0:10] = 0 + arr.mask[1, 0:5, 0:5] = True + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "float32" + assert len(numpy.unique(out.array).tolist()) == 2 # 0.33 and None + assert out.array[0, 0, 0] is numpy.ma.masked + assert out.array[0, 6, 6] is numpy.ma.masked + numpy.testing.assert_almost_equal(out.array[0, 10, 10], 0.3333, decimal=3) + + +def test_hillshade(): + """test hillshade.""" + algo = default_algorithms.get("hillshade")() + + arr = numpy.random.randint(0, 5000, (1, 262, 262), dtype="uint16") + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "uint8" + + arr = numpy.ma.MaskedArray( + numpy.random.randint(0, 5000, (1, 262, 262), dtype="uint16"), + mask=numpy.zeros((1, 262, 262), dtype="bool"), + ) + arr.mask[0, 0:100, 0:100] = True + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 0, 0] is numpy.ma.masked + + +def test_slope(): + """test slope.""" + algo = default_algorithms.get("slope")() + + arr = numpy.random.randint(0, 5000, (1, 262, 262), dtype="uint16") + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "float32" + + arr = numpy.ma.MaskedArray( + numpy.random.randint(0, 5000, (1, 262, 262), dtype="uint16"), + mask=numpy.zeros((1, 262, 262), dtype="bool"), + ) + arr.mask[0, 0:100, 0:100] = True + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "float32" + assert out.array[0, 0, 0] is numpy.ma.masked + + +def test_contours(): + """test contours.""" + algo = default_algorithms.get("contours")() + + arr = numpy.random.randint(0, 5000, (1, 256, 256), dtype="uint16") + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (3, 256, 256) + assert out.array.dtype == "uint8" + + arr = numpy.ma.MaskedArray( + numpy.random.randint(0, 5000, (1, 256, 256), dtype="uint16"), + mask=numpy.zeros((1, 256, 256), dtype="bool"), + ) + arr.mask[0, 0:100, 0:100] = True + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (3, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 0, 0] is numpy.ma.masked + + +def test_terrarium(): + """test terrarium.""" + algo = default_algorithms.get("terrarium")() + + arr = numpy.random.randint(0, 5000, (1, 256, 256), dtype="uint16") + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (3, 256, 256) + assert out.array.dtype == "uint8" + + arr = numpy.ma.MaskedArray( + numpy.random.randint(0, 5000, (1, 256, 256), dtype="uint16"), + mask=numpy.zeros((1, 256, 256), dtype="bool"), + ) + arr.mask[0, 0:100, 0:100] = True + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (3, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 0, 0] is numpy.ma.masked + + # works on the above masked array img, with algo which was passed nodata_height + nodata_height = 10.0 + algo = default_algorithms.get("terrarium")(nodata_height=nodata_height) + out = algo(img) + masked = out.array[:, arr.mask[0, :, :]] + masked_height = (masked[0] * 256 + masked[1] + masked[2] / 256) - 32768 + numpy.testing.assert_array_equal( + masked_height, nodata_height * numpy.ones((100 * 100), dtype="bool") + ) + + +def test_terrainrgb(): + """test terrainrgb.""" + algo = default_algorithms.get("terrainrgb")() + + arr = numpy.random.randint(0, 5000, (1, 256, 256), dtype="uint16") + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (3, 256, 256) + assert out.array.dtype == "uint8" + + arr = numpy.ma.MaskedArray( + numpy.random.randint(0, 5000, (1, 256, 256), dtype="uint16"), + mask=numpy.zeros((1, 256, 256), dtype="bool"), + ) + arr.mask[0, 0:100, 0:100] = True + + img = ImageData(arr) + out = algo(img) + assert out.array.shape == (3, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 0, 0] is numpy.ma.masked + + # works on the above masked array img, with algo which was passed nodata_height + nodata_height = 10.0 + algo = default_algorithms.get("terrainrgb")(nodata_height=nodata_height) + out = algo(img) + masked = out.array[:, arr.mask[0, :, :]] + masked_height = -10000 + ( + ((masked[0] * 256 * 256) + (masked[1] * 256) + masked[2]) * 0.1 + ) + numpy.testing.assert_array_equal( + masked_height, nodata_height * numpy.ones((100 * 100), dtype="bool") + ) + + +def test_ops(): + """test ops: cast, ceil and floor.""" + arr = numpy.ma.MaskedArray( + numpy.random.randint(0, 5000, (1, 256, 256)).astype("float32"), + mask=numpy.zeros((1, 256, 256), dtype="bool"), + ) + arr.data[0, 0, 0] = 1.6 + arr.mask[0, 1:100, 1:100] = True + + img = ImageData(arr) + assert img.array.dtype == numpy.float32 + + algo = default_algorithms.get("cast")() + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 0, 0] == 1 + assert out.array[0, 1, 1] is numpy.ma.masked + + assert img.array.dtype == numpy.float32 + algo = default_algorithms.get("floor")() + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 0, 0] == 1 + assert out.array[0, 1, 1] is numpy.ma.masked + + assert img.array.dtype == numpy.float32 + algo = default_algorithms.get("ceil")() + out = algo(img) + assert out.array.shape == (1, 256, 256) + assert out.array.dtype == "uint8" + assert out.array[0, 0, 0] == 2 + assert out.array[0, 1, 1] is numpy.ma.masked + + +@pytest.mark.parametrize( + "name,numpy_method,options", + [ + ("min", numpy.ma.min, {}), + ("max", numpy.ma.max, {}), + ("median", numpy.ma.median, {}), + ("mean", numpy.ma.mean, {}), + ("std", numpy.ma.std, {"ddof": 1}), + ("var", numpy.ma.var, {"ddof": 1}), + ], +) +def test_math_algorithm(name, numpy_method, options): + """test math algos.""" + arr = numpy.ma.MaskedArray( + numpy.random.randint(0, 5000, (1, 256, 256)).astype("float32"), + mask=numpy.zeros((1, 256, 256), dtype="bool"), + ) + arr.data[0, 0, 0] = 1.6 + arr.mask[0, 1:100, 1:100] = True + + img = ImageData(arr) + assert img.array.dtype == numpy.float32 + + algo = default_algorithms.get(name)() + out = algo(img) + + numpy.testing.assert_array_equal( + out.array, numpy_method(img.array, axis=0, keepdims=True, **options) + ) + assert out.array[0, 1, 1] is numpy.ma.masked diff --git a/src/titiler/core/tests/test_cache_middleware.py b/src/titiler/core/tests/test_cache_middleware.py index b1331edd9..d6b17d095 100644 --- a/src/titiler/core/tests/test_cache_middleware.py +++ b/src/titiler/core/tests/test_cache_middleware.py @@ -1,9 +1,9 @@ """Test titiler.core.CacheControlMiddleware.""" - from fastapi import FastAPI, Path from starlette.responses import Response from starlette.testclient import TestClient +from typing_extensions import Annotated from titiler.core.middleware import CacheControlMiddleware @@ -29,18 +29,18 @@ async def route3(): @app.get("/tiles/{z}/{x}/{y}") async def tiles( - z: int = Path(..., ge=0, le=30, description="Mercator tiles's zoom level"), - x: int = Path(..., description="Mercator tiles's column"), - y: int = Path(..., description="Mercator tiles's row"), + z: Annotated[int, Path(ge=0, le=30, description="Mercator tiles's zoom level")], + x: Annotated[int, Path(description="Mercator tiles's column")], + y: Annotated[int, Path(description="Mercator tiles's row")], ): """tiles.""" return "yeah" @app.get("/emptytiles/{z}/{x}/{y}") async def emptytiles( - z: int = Path(..., ge=0, le=30, description="Mercator tiles's zoom level"), - x: int = Path(..., description="Mercator tiles's column"), - y: int = Path(..., description="Mercator tiles's row"), + z: Annotated[int, Path(ge=0, le=30, description="Mercator tiles's zoom level")], + x: Annotated[int, Path(description="Mercator tiles's column")], + y: Annotated[int, Path(description="Mercator tiles's row")], ): """tiles.""" return Response(status_code=404) diff --git a/src/titiler/core/tests/test_case_middleware.py b/src/titiler/core/tests/test_case_middleware.py index dc46ab5ae..0a13abe7f 100644 --- a/src/titiler/core/tests/test_case_middleware.py +++ b/src/titiler/core/tests/test_case_middleware.py @@ -4,6 +4,7 @@ from fastapi import FastAPI, Query from starlette.testclient import TestClient +from typing_extensions import Annotated from titiler.core.middleware import LowerCaseQueryStringMiddleware @@ -13,7 +14,7 @@ def test_lowercase_middleware(): app = FastAPI() @app.get("/route1") - async def route1(value: str = Query(...)): + async def route1(value: Annotated[str, Query()]): """route1.""" return {"value": value} @@ -33,7 +34,7 @@ def test_lowercase_middleware_multiple_values(): app = FastAPI() @app.get("/route1") - async def route1(value: List[str] = Query(...)): + async def route1(value: Annotated[List[str], Query()]): """route1.""" return {"value": value} @@ -46,3 +47,26 @@ async def route1(value: List[str] = Query(...)): response = client.get("/route1?VALUE=lorenzori&VALUE=dogs&value=trucks") assert response.json() == {"value": ["lorenzori", "dogs", "trucks"]} + + +def test_lowercase_middleware_url_with_query_parameters(): + """Make sure all query parameters return.""" + app = FastAPI() + + @app.get("/route1") + async def route1(url: List[str] = Query(...)): + """route1.""" + return {"url": url} + + app.add_middleware(LowerCaseQueryStringMiddleware) + + client = TestClient(app) + + url = "https://developmentseed.org?solutions=geospatial&planet=better" + url_encoded = ( + "https%3A%2F%2Fdevelopmentseed.org%3Fsolutions%3Dgeospatial%26planet%3Dbetter" + ) + + response = client.get(f"/route1?url={url_encoded}") + + assert response.json() == {"url": [url]} diff --git a/src/titiler/core/tests/test_dependencies.py b/src/titiler/core/tests/test_dependencies.py index 6254aa471..fbbb1d358 100644 --- a/src/titiler/core/tests/test_dependencies.py +++ b/src/titiler/core/tests/test_dependencies.py @@ -2,13 +2,14 @@ import json from dataclasses import dataclass -from typing import Literal +from typing import Literal, Optional import pytest -from fastapi import Depends, FastAPI, Query +from fastapi import Depends, FastAPI, Path from morecantile import tms from rio_tiler.types import ColorMapType from starlette.testclient import TestClient +from typing_extensions import Annotated from titiler.core import dependencies, errors from titiler.core.resources.responses import JSONResponse @@ -18,15 +19,20 @@ def test_tms(): """Create App.""" app = FastAPI() - @app.get("/web/{TileMatrixSetId}") - def web(TileMatrixSetId: Literal["WebMercatorQuad"] = Query(...)): + @app.get("/web/{tileMatrixSetId}") + def web( + tileMatrixSetId: Annotated[ + Literal["WebMercatorQuad"], + Path(), + ], + ): """return tms id.""" - return TileMatrixSetId + return tileMatrixSetId - @app.get("/all/{TileMatrixSetId}") - def all(TileMatrixSetId: Literal[tuple(tms.list())] = Query(...)): + @app.get("/all/{tileMatrixSetId}") + def all(tileMatrixSetId: Annotated[Literal[tuple(tms.list())], Path()]): """return tms id.""" - return TileMatrixSetId + return tileMatrixSetId client = TestClient(app) response = client.get("/web/WebMercatorQuad") @@ -34,7 +40,7 @@ def all(TileMatrixSetId: Literal[tuple(tms.list())] = Query(...)): response = client.get("/web/WorldCRS84Quad") assert response.status_code == 422 - assert "permitted: 'WebMercatorQuad'" in response.json()["detail"][0]["msg"] + assert "Input should be 'WebMercatorQuad'" in response.json()["detail"][0]["msg"] response = client.get("/all/WebMercatorQuad") assert response.json() == "WebMercatorQuad" @@ -60,7 +66,7 @@ def main(cm=Depends(dependencies.ColorMapParams)): assert response.json()["1"] == [68, 2, 85, 255] cmap = json.dumps({1: [68, 1, 84, 255]}) - response = client.get(f"/?colormap={cmap}") + response = client.get("/", params={"colormap": cmap}) assert response.json()["1"] == [68, 1, 84, 255] cmap = json.dumps({0: "#000000", 255: "#ffffff"}) @@ -75,7 +81,7 @@ def main(cm=Depends(dependencies.ColorMapParams)): ([3, 1000], [255, 0, 0, 255]), ] cmap = json.dumps(intervals) - response = client.get(f"/?colormap={cmap}") + response = client.get("/", params={"colormap": cmap}) assert response.json()[0] == [[1, 2], [0, 0, 0, 255]] assert response.json()[1] == [[2, 3], [255, 255, 255, 255]] assert response.json()[2] == [[3, 1000], [255, 0, 0, 255]] @@ -141,11 +147,11 @@ def test_default(): @dataclass class dep(dependencies.DefaultDependency): + v: Optional[int] = None - v: int - - # make sure we can unpack the class - assert dict(**dep(v=1)) == {"v": 1} + assert dep(v=1).as_dict() == {"v": 1} + assert dep().as_dict() == {} + assert dep().as_dict(exclude_none=False) == {"v": None} assert dep(v=1).v == 1 @@ -240,6 +246,18 @@ def _assets_bidx(params=Depends(dependencies.AssetsBidxParams)): response = client.get("/third?assets=data&assets=image") assert response.json()["assets"] == ["data", "image"] + response = client.get( + "/third", + params=( + ("assets", "data"), + ("assets", "image"), + ), + ) + assert response.json()["assets"] == ["data", "image"] + + response = client.get("/third", params={"assets": ["data", "image"]}) + assert response.json()["assets"] == ["data", "image"] + response = client.get("/third") assert not response.json()["assets"] @@ -249,12 +267,37 @@ def _assets_bidx(params=Depends(dependencies.AssetsBidxParams)): assert response.json()["assets"] == ["data", "image"] assert response.json()["asset_indexes"] == {"data": [1, 2, 3], "image": [1]} + response = client.get( + "/third", + params=( + ("assets", "data"), + ("assets", "image"), + ("asset_bidx", "data|1,2,3"), + ("asset_bidx", "image|1"), + ), + ) + + assert response.json()["assets"] == ["data", "image"] + assert response.json()["asset_indexes"] == {"data": [1, 2, 3], "image": [1]} + response = client.get( "/third?assets=data&assets=image&asset_expression=data|b1/b2&asset_expression=image|b1*b2" ) assert response.json()["assets"] == ["data", "image"] assert response.json()["asset_expression"] == {"data": "b1/b2", "image": "b1*b2"} + response = client.get( + "/third", + params=( + ("assets", "data"), + ("assets", "image"), + ("asset_expression", "data|b1/b2"), + ("asset_expression", "image|b1*b2"), + ), + ) + assert response.json()["assets"] == ["data", "image"] + assert response.json()["asset_expression"] == {"data": "b1/b2", "image": "b1*b2"} + def test_bands(): """test bands deps.""" @@ -302,33 +345,70 @@ def _bands_expr_opt(params=Depends(dependencies.BandsExprParamsOptional)): assert not response.json()["bands"] -def test_image(): - """test image deps.""" +def test_preview_part_params(): + """test preview/part deps.""" app = FastAPI() - @app.get("/") - def _endpoint(params=Depends(dependencies.ImageParams)): + @app.get("/preview") + @app.get("/preview/{width}x{height}") + def _endpoint(params=Depends(dependencies.PreviewParams)): + """return params.""" + return params + + @app.get("/part") + @app.get("/part/{width}x{height}") + def _endpoint(params=Depends(dependencies.PartFeatureParams)): """return params.""" return params client = TestClient(app) - response = client.get("/") + response = client.get("/preview") assert response.json()["max_size"] == 1024 assert not response.json()["height"] assert not response.json()["width"] - response = client.get("/?max_size=2048") + response = client.get("/preview?max_size=2048") assert response.json()["max_size"] == 2048 assert not response.json()["height"] assert not response.json()["width"] - response = client.get("/?width=128") - assert response.json()["max_size"] == 1024 + response = client.get("/preview?width=128") + assert not response.json()["max_size"] assert not response.json()["height"] assert response.json()["width"] == 128 - response = client.get("/?width=128&height=128") + response = client.get("/preview/128x128") + assert not response.json()["max_size"] + assert response.json()["height"] == 128 + assert response.json()["width"] == 128 + + response = client.get("/preview?width=128&height=128") + assert not response.json()["max_size"] + assert response.json()["height"] == 128 + assert response.json()["width"] == 128 + + response = client.get("/part") + assert not response.json()["max_size"] + assert not response.json()["height"] + assert not response.json()["width"] + + response = client.get("/part?max_size=2048") + assert response.json()["max_size"] == 2048 + assert not response.json()["height"] + assert not response.json()["width"] + + response = client.get("/part?width=128") + assert not response.json()["max_size"] + assert not response.json()["height"] + assert response.json()["width"] == 128 + + response = client.get("/part?width=128&height=128") + assert not response.json()["max_size"] + assert response.json()["height"] == 128 + assert response.json()["width"] == 128 + + response = client.get("/part/128x128") assert not response.json()["max_size"] assert response.json()["height"] == 128 assert response.json()["width"] == 128 @@ -353,7 +433,7 @@ def is_nan(params=Depends(dependencies.DatasetParams)): response = client.get("/") assert not response.json()["nodata"] assert not response.json()["unscale"] - assert response.json()["resampling_method"] == "nearest" + assert not response.json()["resampling_method"] response = client.get("/?resampling=cubic") assert not response.json()["nodata"] @@ -379,7 +459,7 @@ def _endpoint(params=Depends(dependencies.ImageRenderingParams)): client = TestClient(app) response = client.get("/") - assert response.json()["add_mask"] is True + assert not response.json()["add_mask"] response = client.get("/?return_mask=False") assert response.json()["add_mask"] is False @@ -401,7 +481,7 @@ def test_algo(): def _endpoint(algorithm=Depends(PostProcessParams)): """return params.""" if algorithm: - return algorithm.dict() + return algorithm.model_dump() return {} client = TestClient(app) @@ -412,7 +492,7 @@ def _endpoint(algorithm=Depends(PostProcessParams)): assert response.status_code == 422 response = client.get("/?algorithm=hillshade") - assert response.json()["azimuth"] == 90 + assert response.json()["azimuth"] == 45 assert response.json()["buffer"] == 3 assert response.json()["input_nbands"] == 1 @@ -426,3 +506,103 @@ def _endpoint(algorithm=Depends(PostProcessParams)): assert response.json()["azimuth"] == 30 assert response.json()["buffer"] == 4 assert response.json()["input_nbands"] == 1 + + +def test_rescale_params(): + """test RescalingParams dependency.""" + app = FastAPI() + + @app.get("/") + def main(params=Depends(dependencies.ImageRenderingParams)): + """return rescale.""" + return params.rescale + + client = TestClient(app) + + response = client.get("/", params={"rescale": "0,1"}) + assert response.status_code == 200 + assert response.json() == [[0, 1]] + + response = client.get("/?rescale=0,1") + assert response.status_code == 200 + assert response.json() == [[0, 1]] + + response = client.get("/?rescale=0,1&rescale=2,3") + assert response.status_code == 200 + assert response.json() == [[0, 1], [2, 3]] + + with pytest.raises(AssertionError): + client.get("/", params={"rescale": [0, 1]}) + + response = client.get("/", params={"rescale": [[0, 1]]}) + assert response.status_code == 200 + assert response.json() == [[0, 1]] + + response = client.get( + "/", + params=( + ("rescale", [0, 1]), + ("rescale", [0, 1]), + ), + ) + assert response.status_code == 200 + assert response.json() == [[0, 1], [0, 1]] + + response = client.get( + "/", + params=( + ("rescale", "0,1"), + ("rescale", "0,1"), + ), + ) + assert response.status_code == 200 + assert response.json() == [[0, 1], [0, 1]] + + response = client.get("/", params={"rescale": [[0, 1], [2, 3]]}) + assert response.status_code == 200 + assert response.json() == [[0, 1], [2, 3]] + + +def test_histogram_params(): + """Test HistogramParams dependency.""" + app = FastAPI() + + @app.get("/") + def main(params=Depends(dependencies.HistogramParams)): + """return rescale.""" + return params + + client = TestClient(app) + + response = client.get( + "/", + params={"histogram_bins": "8"}, + ) + assert response.status_code == 200 + assert response.json()["bins"] == 8 + + response = client.get( + "/", + params={"histogram_bins": "8,9"}, + ) + assert response.status_code == 200 + assert response.json()["bins"] == [8.0, 9.0] + + response = client.get( + "/", + ) + assert response.status_code == 200 + assert response.json()["bins"] == 10 + + response = client.get( + "/", + params={"histogram_range": "8,9"}, + ) + assert response.status_code == 200 + assert response.json()["range"] == [8.0, 9.0] + + with pytest.raises(AssertionError): + client.get( + "/", + params={"histogram_range": "8"}, + ) diff --git a/src/titiler/core/tests/test_factories.py b/src/titiler/core/tests/test_factories.py index 408cf96a9..1760e050f 100644 --- a/src/titiler/core/tests/test_factories.py +++ b/src/titiler/core/tests/test_factories.py @@ -3,10 +3,12 @@ import json import os import pathlib +import warnings +import xml.etree.ElementTree as ET from dataclasses import dataclass from enum import Enum from io import BytesIO -from typing import Dict, Optional, Type +from typing import Dict, Optional, Sequence, Type from unittest.mock import patch from urllib.parse import urlencode @@ -14,19 +16,25 @@ import httpx import morecantile import numpy +import pytest +from attrs import define from fastapi import Depends, FastAPI, HTTPException, Path, Query, security, status from morecantile.defaults import TileMatrixSets from rasterio.crs import CRS from rasterio.io import MemoryFile +from rio_tiler.colormap import cmap as default_cmap +from rio_tiler.errors import InvalidDatatypeWarning, NoOverviewWarning from rio_tiler.io import BaseReader, MultiBandReader, Reader, STACReader from starlette.requests import Request from starlette.testclient import TestClient +from typing_extensions import Annotated -from titiler.core.dependencies import RescaleType +from titiler.core import dependencies from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers from titiler.core.factory import ( AlgorithmFactory, - BaseTilerFactory, + BaseFactory, + ColorMapFactory, MultiBandTilerFactory, MultiBaseTilerFactory, TilerFactory, @@ -43,7 +51,7 @@ def test_TilerFactory(): """Test TilerFactory class.""" cog = TilerFactory() - assert len(cog.router.routes) == 27 + assert len(cog.router.routes) == 23 assert len(cog.supported_tms.list()) == NB_DEFAULT_TMS cog = TilerFactory(router_prefix="something", supported_tms=WEB_TMS) @@ -53,7 +61,15 @@ def test_TilerFactory(): app.include_router(cog.router, prefix="/something") client = TestClient(app) - response = client.get(f"/something/tilejson.json?url={DATA_DIR}/cog.tif") + response = client.get("/openapi.json") + assert response.status_code == 200 + + response = client.get("/docs") + assert response.status_code == 200 + + response = client.get( + f"/something/WebMercatorQuad/tilejson.json?url={DATA_DIR}/cog.tif" + ) assert response.status_code == 200 assert response.headers["content-type"] == "application/json" assert response.json()["tilejson"] @@ -62,7 +78,7 @@ def test_TilerFactory(): assert response.status_code == 422 cog = TilerFactory(add_preview=False, add_part=False, add_viewer=False) - assert len(cog.router.routes) == 18 + assert len(cog.router.routes) == 14 app = FastAPI() cog = TilerFactory() @@ -72,17 +88,19 @@ def test_TilerFactory(): client = TestClient(app) - response = client.get(f"/tiles/8/87/48?url={DATA_DIR}/cog.tif&rescale=0,1000") + response = client.get( + f"/tiles/WebMercatorQuad/8/87/48?url={DATA_DIR}/cog.tif&rescale=0,1000" + ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" response = client.get( - f"/tiles/8/87/48?url={DATA_DIR}/cog.tif&rescale=-3.4028235e+38,3.4028235e+38" + f"/tiles/WebMercatorQuad/8/87/48?url={DATA_DIR}/cog.tif&rescale=-3.4028235e+38,3.4028235e+38" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" response = client.get( - f"/tiles/8/87/48.tif?url={DATA_DIR}/cog.tif&bidx=1&bidx=1&bidx=1&return_mask=false" + f"/tiles/WebMercatorQuad/8/87/48.tif?url={DATA_DIR}/cog.tif&bidx=1&bidx=1&bidx=1&return_mask=false" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/tiff; application=geotiff" @@ -93,7 +111,7 @@ def test_TilerFactory(): assert meta["height"] == 256 response = client.get( - "/tiles/8/87/48.tif", + "/tiles/WebMercatorQuad/8/87/48.tif", params={ "url": f"{DATA_DIR}/cog.tif", "expression": "b1;b1;b1", @@ -109,14 +127,17 @@ def test_TilerFactory(): assert meta["height"] == 256 response = client.get( - f"/tiles/8/84/47?url={DATA_DIR}/cog.tif&bidx=1&rescale=0,1000&colormap_name=viridis" + f"/tiles/WebMercatorQuad/8/84/47?url={DATA_DIR}/cog.tif&bidx=1&rescale=0,1000&colormap_name=viridis" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" # Dict - cmap = urlencode( - { + response = client.get( + "/tiles/WebMercatorQuad/8/84/47.png", + params={ + "url": f"{DATA_DIR}/cog.tif", + "bidx": 1, "colormap": json.dumps( { "1": [58, 102, 24, 255], @@ -124,16 +145,18 @@ def test_TilerFactory(): "3": "#b1b129", "4": "#ddcb9aFF", } - ) - } + ), + }, ) - response = client.get(f"/tiles/8/84/47.png?url={DATA_DIR}/cog.tif&bidx=1&{cmap}") assert response.status_code == 200 assert response.headers["content-type"] == "image/png" # Intervals - cmap = urlencode( - { + response = client.get( + "/tiles/WebMercatorQuad/8/84/47.png", + params={ + "url": f"{DATA_DIR}/cog.tif", + "bidx": 1, "colormap": json.dumps( [ # ([min, max], [r, g, b, a]) @@ -141,32 +164,37 @@ def test_TilerFactory(): ([2, 3], [255, 255, 255, 255]), ([3, 1000], [255, 0, 0, 255]), ] - ) - } + ), + }, ) - response = client.get(f"/tiles/8/84/47.png?url={DATA_DIR}/cog.tif&bidx=1&{cmap}") assert response.status_code == 200 assert response.headers["content-type"] == "image/png" # Bad colormap format cmap = urlencode({"colormap": json.dumps({"1": [58, 102]})}) - response = client.get(f"/tiles/8/84/47.png?url={DATA_DIR}/cog.tif&bidx=1&{cmap}") + response = client.get( + f"/tiles/WebMercatorQuad/8/84/47.png?url={DATA_DIR}/cog.tif&bidx=1&{cmap}" + ) assert response.status_code == 400 # no json encoding cmap = urlencode({"colormap": {"1": [58, 102]}}) - response = client.get(f"/tiles/8/84/47.png?url={DATA_DIR}/cog.tif&bidx=1&{cmap}") + response = client.get( + f"/tiles/WebMercatorQuad/8/84/47.png?url={DATA_DIR}/cog.tif&bidx=1&{cmap}" + ) assert response.status_code == 400 # Test NumpyTile - response = client.get(f"/tiles/8/87/48.npy?url={DATA_DIR}/cog.tif") + response = client.get(f"/tiles/WebMercatorQuad/8/87/48.npy?url={DATA_DIR}/cog.tif") assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" npy_tile = numpy.load(BytesIO(response.content)) assert npy_tile.shape == (2, 256, 256) # mask + data # Test Buffer - response = client.get(f"/tiles/8/87/48.npy?url={DATA_DIR}/cog.tif&buffer=10") + response = client.get( + f"/tiles/WebMercatorQuad/8/87/48.npy?url={DATA_DIR}/cog.tif&buffer=10" + ) assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" npy_tile = numpy.load(BytesIO(response.content)) @@ -179,19 +207,37 @@ def test_TilerFactory(): assert response.headers["content-type"] == "image/jpeg" response = client.get( - f"/crop/-56.228,72.715,-54.547,73.188.png?url={DATA_DIR}/cog.tif&rescale=0,1000&max_size=256" + f"/bbox/-56.228,72.715,-54.547,73.188.png?url={DATA_DIR}/cog.tif&rescale=0,1000&max_size=256" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" + response = client.get( + f"/bbox/-56.228,72.715,-54.547,73.188/100x100.png?url={DATA_DIR}/cog.tif&rescale=0,1000" + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/png" + meta = parse_img(response.content) + assert meta["driver"] == "PNG" + assert meta["count"] == 2 + assert meta["width"] == 100 + assert meta["height"] == 100 + response = client.get(f"/point/-56.228,72.715?url={DATA_DIR}/cog.tif") assert response.status_code == 200 assert response.headers["content-type"] == "application/json" assert len(response.json()["values"]) == 1 assert response.json()["band_names"] == ["b1"] + # Masked values + response = client.get(f"/point/-59.337,73.9898?url={DATA_DIR}/cog.tif&nodata=1") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + assert response.json()["values"] == [None] + assert response.json()["band_names"] == ["b1"] + response = client.get( - f"/point/-6259272.328324187,12015838.020930404?url={DATA_DIR}/cog.tif&coord-crs=EPSG:3857" + f"/point/-6259272.328324187,12015838.020930404?url={DATA_DIR}/cog.tif&coord_crs=EPSG:3857" ) assert response.status_code == 200 assert response.headers["content-type"] == "application/json" @@ -208,7 +254,7 @@ def test_TilerFactory(): assert len(response.json()["values"]) == 1 assert response.json()["band_names"] == ["b1*2"] - response = client.get(f"/tilejson.json?url={DATA_DIR}/cog.tif") + response = client.get(f"/WebMercatorQuad/tilejson.json?url={DATA_DIR}/cog.tif") assert response.status_code == 200 assert response.headers["content-type"] == "application/json" assert response.json()["tilejson"] @@ -218,18 +264,17 @@ def test_TilerFactory(): assert response.headers["content-type"] == "application/json" assert response.json()["tilejson"] - response_qs = client.get( - f"/tilejson.json?url={DATA_DIR}/cog.tif&TileMatrixSetId=WorldCRS84Quad" + response = client.get( + f"/WebMercatorQuad/tilejson.json?url={DATA_DIR}/cog.tif&tile_format=png" ) - assert response.json()["tiles"] == response_qs.json()["tiles"] - - response = client.get(f"/tilejson.json?url={DATA_DIR}/cog.tif&tile_format=png") assert response.status_code == 200 assert response.headers["content-type"] == "application/json" assert response.json()["tilejson"] assert "png" in response.json()["tiles"][0] - response = client.get(f"/tilejson.json?url={DATA_DIR}/cog.tif&minzoom=5&maxzoom=12") + response = client.get( + f"/WebMercatorQuad/tilejson.json?url={DATA_DIR}/cog.tif&minzoom=5&maxzoom=12" + ) assert response.status_code == 200 assert response.headers["content-type"] == "application/json" assert response.json()["tilejson"] @@ -237,13 +282,26 @@ def test_TilerFactory(): assert response.json()["maxzoom"] == 12 response = client.get( - f"/WMTSCapabilities.xml?url={DATA_DIR}/cog.tif&minzoom=5&maxzoom=12" + f"/WebMercatorQuad/WMTSCapabilities.xml?url={DATA_DIR}/cog.tif&minzoom=5&maxzoom=12" ) assert response.status_code == 200 assert response.headers["content-type"] == "application/xml" meta = parse_img(response.content) assert meta["driver"] == "WMTS" assert meta["crs"] == "EPSG:3857" + root = ET.fromstring(response.content) + assert root is not None + + response = client.get( + f"/WebMercatorQuad/WMTSCapabilities.xml?url={DATA_DIR}/cog.tif&bdix=1&rescale=0,1000" + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "application/xml" + meta = parse_img(response.content) + assert meta["driver"] == "WMTS" + assert meta["crs"] == "EPSG:3857" + root = ET.fromstring(response.content) + assert root is not None response = client.get( f"/WorldCRS84Quad/WMTSCapabilities.xml?url={DATA_DIR}/cog.tif&minzoom=5&maxzoom=12" @@ -253,11 +311,14 @@ def test_TilerFactory(): meta = parse_img(response.content) assert meta["driver"] == "WMTS" assert str(meta["crs"]) == "OGC:CRS84" + root = ET.fromstring(response.content) + assert root is not None response = client.get(f"/bounds?url={DATA_DIR}/cog.tif") assert response.status_code == 200 assert response.headers["content-type"] == "application/json" - assert response.json()["bounds"] + assert len(response.json()["bounds"]) == 4 + assert response.json()["crs"] response = client.get(f"/info?url={DATA_DIR}/cog.tif") assert response.status_code == 200 @@ -268,6 +329,17 @@ def test_TilerFactory(): assert response.status_code == 200 assert response.headers["content-type"] == "application/geo+json" assert response.json()["type"] == "Feature" + assert "bbox" in response.json() + assert response.json()["geometry"]["type"] == "Polygon" + + # BBOX crossing the Antimeridian + with pytest.warns(UserWarning): + response = client.get(f"/info.geojson?url={DATA_DIR}/cog_dateline.tif") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/geo+json" + assert response.json()["type"] == "Feature" + assert "bbox" in response.json() + assert response.json()["geometry"]["type"] == "MultiPolygon" response = client.get( f"/preview.png?url={DATA_DIR}/cog.tif&rescale=0,1000&max_size=256" @@ -286,14 +358,12 @@ def test_TilerFactory(): assert meta["width"] == 512 assert meta["height"] == 512 - response = client.get( - f"/preview.png?url={DATA_DIR}/cog.tif&rescale=0,1000&max_size=0&nodata=0" - ) + response = client.get(f"/preview/512x512.png?url={DATA_DIR}/cog.tif&rescale=0,1000") assert response.status_code == 200 assert response.headers["content-type"] == "image/png" meta = parse_img(response.content) - assert meta["width"] == 2658 - assert meta["height"] == 2667 + assert meta["width"] == 512 + assert meta["height"] == 512 response = client.get( f"/preview.png?url={DATA_DIR}/cog.tif&rescale=0,1000&max_size=0&nodata=0" @@ -345,18 +415,31 @@ def test_TilerFactory(): feature_collection = {"type": "FeatureCollection", "features": [feature]} - response = client.post(f"/crop?url={DATA_DIR}/cog.tif", json=feature) + response = client.post(f"/feature?url={DATA_DIR}/cog.tif", json=feature) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/png" + + response = client.post( + f"/feature/100x100.png?url={DATA_DIR}/cog.tif&rescale=0,1000", json=feature + ) assert response.status_code == 200 assert response.headers["content-type"] == "image/png" + meta = parse_img(response.content) + assert meta["driver"] == "PNG" + assert meta["width"] == 100 + assert meta["height"] == 100 - response = client.post(f"/crop.tif?url={DATA_DIR}/cog.tif", json=feature) + response = client.post(f"/feature.tif?url={DATA_DIR}/cog.tif", json=feature) assert response.status_code == 200 assert response.headers["content-type"] == "image/tiff; application=geotiff" meta = parse_img(response.content) assert meta["dtype"] == "uint16" assert meta["count"] == 2 - response = client.post(f"/crop/100x100.jpeg?url={DATA_DIR}/cog.tif", json=feature) + with pytest.warns(InvalidDatatypeWarning): + response = client.post( + f"/feature/100x100.jpeg?url={DATA_DIR}/cog.tif", json=feature + ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" meta = parse_img(response.content) @@ -510,6 +593,16 @@ def test_TilerFactory(): assert min(resp["b1"]["histogram"][1]) == 5.0 assert max(resp["b1"]["histogram"][1]) == 10.0 + # Stats with Algorithm + response = client.get( + f"/statistics?url={DATA_DIR}/cog.tif&bidx=1&bidx=1&algorithm=normalizedIndex" + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + assert len(resp) == 1 + assert "(b1 - b1) / (b1 + b1)" in resp + # POST - statistics response = client.post( f"/statistics?url={DATA_DIR}/cog.tif&bidx=1&bidx=1&bidx=1", json=feature @@ -567,7 +660,9 @@ def test_TilerFactory(): } response = client.post( - f"/statistics?url={DATA_DIR}/cog.tif&categorical=true", json=feature + "/statistics", + json=feature, + params={"categorical": True, "max_size": 1024, "url": f"{DATA_DIR}/cog.tif"}, ) assert response.status_code == 200 assert response.headers["content-type"] == "application/geo+json" @@ -595,8 +690,17 @@ def test_TilerFactory(): assert len(resp["properties"]["statistics"]["b1"]["histogram"][1]) == 12 response = client.post( - f"/statistics?url={DATA_DIR}/cog.tif&categorical=true&c=1&c=2&c=3&c=4", + "/statistics", json=feature, + params=( + ("categorical", True), + ("c", 1), + ("c", 2), + ("c", 3), + ("c", 4), + ("max_size", 1024), + ("url", f"{DATA_DIR}/cog.tif"), + ), ) assert response.status_code == 200 assert response.headers["content-type"] == "application/geo+json" @@ -624,6 +728,18 @@ def test_TilerFactory(): assert len(resp["properties"]["statistics"]["b1"]["histogram"][0]) == 4 assert resp["properties"]["statistics"]["b1"]["histogram"][0][3] == 0 + # Stats with Algorithm + response = client.post( + f"/statistics?url={DATA_DIR}/cog.tif&bidx=1&bidx=1&algorithm=normalizedIndex", + json=feature, + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "application/geo+json" + resp = response.json() + assert resp["type"] == "Feature" + assert len(resp["properties"]["statistics"]) == 1 + assert "(b1 - b1) / (b1 + b1)" in resp["properties"]["statistics"] + # Test with Algorithm response = client.get(f"/preview.tif?url={DATA_DIR}/dem.tif&return_mask=False") assert response.status_code == 200 @@ -641,6 +757,25 @@ def test_TilerFactory(): assert meta["dtype"] == "uint8" assert meta["count"] == 3 + # OGC Tileset + response = client.get(f"/tiles?url={DATA_DIR}/cog.tif") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + assert len(resp["tilesets"]) == NB_DEFAULT_TMS + + first_tms = resp["tilesets"][0] + first_id = DEFAULT_TMS.list()[0] + assert first_id in first_tms["title"] + assert len(first_tms["links"]) == 2 # no link to the tms definition + + response = client.get(f"/tiles/WebMercatorQuad?url={DATA_DIR}/cog.tif") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + # covers only 5 zoom levels + assert len(resp["tileMatrixSetLimits"]) == 5 + @patch("rio_tiler.io.rasterio.rasterio") def test_MultiBaseTilerFactory(rio): @@ -648,7 +783,7 @@ def test_MultiBaseTilerFactory(rio): rio.open = mock_rasterio_open stac = MultiBaseTilerFactory(reader=STACReader) - assert len(stac.router.routes) == 29 + assert len(stac.router.routes) == 25 app = FastAPI() app.include_router(stac.router) @@ -657,6 +792,12 @@ def test_MultiBaseTilerFactory(rio): client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + + response = client.get("/docs") + assert response.status_code == 200 + response = client.get(f"/assets?url={DATA_DIR}/item.json") assert response.status_code == 200 assert len(response.json()) == 2 @@ -664,10 +805,13 @@ def test_MultiBaseTilerFactory(rio): response = client.get(f"/bounds?url={DATA_DIR}/item.json") assert response.status_code == 200 assert len(response.json()["bounds"]) == 4 + assert response.json()["crs"] - response = client.get(f"/info?url={DATA_DIR}/item.json") - assert response.status_code == 200 - assert len(response.json()) == 2 + # no assets + with pytest.warns(UserWarning): + response = client.get(f"/info?url={DATA_DIR}/item.json") + assert response.status_code == 200 + assert len(response.json()) == 2 response = client.get(f"/info?url={DATA_DIR}/item.json&assets=B01&assets=B09") assert response.status_code == 200 @@ -714,6 +858,25 @@ def test_MultiBaseTilerFactory(rio): assert meta["dtype"] == "uint16" assert meta["count"] == 3 + response = client.get( + f"/preview.tif?url={DATA_DIR}/item.json&assets=B01&bidx=1&bidx=1&return_mask=false" + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/tiff; application=geotiff" + meta = parse_img(response.content) + assert meta["dtype"] == "uint16" + assert meta["count"] == 2 + + with pytest.warns(UserWarning): + response = client.get( + f"/preview.tif?url={DATA_DIR}/item.json&assets=B01&asset_bidx=B01|1,1,1&bidx=1&return_mask=false" + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "image/tiff; application=geotiff" + meta = parse_img(response.content) + assert meta["dtype"] == "uint16" + assert meta["count"] == 3 + response = client.get( "/preview.tif", params={ @@ -814,6 +977,15 @@ def test_MultiBaseTilerFactory(rio): assert resp["B01_b1"] assert resp["B09_b1"] + # with Algorithm + response = client.get( + f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09&algorithm=normalizedIndex&asset_as_band=True" + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + assert "(B09 - B01) / (B09 + B01)" in resp + stac_feature = { "type": "FeatureCollection", "features": [ @@ -923,6 +1095,37 @@ def test_MultiBaseTilerFactory(rio): } assert props["B09_b1"] + # with Algorithm + response = client.post( + f"/statistics?url={DATA_DIR}/item.json&assets=B01&assets=B09&algorithm=normalizedIndex&asset_as_band=True", + json=stac_feature["features"][0], + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "application/geo+json" + resp = response.json() + props = resp["properties"]["statistics"] + assert len(props) == 1 + assert "(B09 - B01) / (B09 + B01)" in props + + # OGC Tileset + response = client.get(f"/tiles?url={DATA_DIR}/item.json") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + assert len(resp["tilesets"]) == NB_DEFAULT_TMS + + first_tms = resp["tilesets"][0] + first_id = DEFAULT_TMS.list()[0] + assert first_id in first_tms["title"] + assert len(first_tms["links"]) == 2 # no link to the tms definition + + response = client.get(f"/tiles/WebMercatorQuad?url={DATA_DIR}/item.json") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + # default minzoom/maxzoom are 0->24 + assert len(resp["tileMatrixSetLimits"]) == 25 + @attr.s class BandFileReader(MultiBandReader): @@ -932,20 +1135,15 @@ class BandFileReader(MultiBandReader): tms: morecantile.TileMatrixSet = attr.ib( default=morecantile.tms.get("WebMercatorQuad") ) - reader_options: Dict = attr.ib(factory=dict) reader: Type[BaseReader] = attr.ib(default=Reader) + reader_options: Dict = attr.ib(factory=dict) - minzoom: int = attr.ib() - maxzoom: int = attr.ib() - - @minzoom.default - def _minzoom(self): - return self.tms.minzoom + bands: Sequence[str] = attr.ib(init=False) + default_bands: Optional[Sequence[str]] = attr.ib(init=False, default=None) - @maxzoom.default - def _maxzoom(self): - return self.tms.maxzoom + minzoom: int = attr.ib(init=False) + maxzoom: int = attr.ib(init=False) def __attrs_post_init__(self): """Parse Sceneid and get grid bounds.""" @@ -955,6 +1153,9 @@ def __attrs_post_init__(self): self.crs = cog.crs self.minzoom = cog.minzoom self.maxzoom = cog.maxzoom + self.width = cog.width + self.height = cog.height + self.transform = cog.transform def _get_band_url(self, band: str) -> str: """Validate band's name and return band's url.""" @@ -972,7 +1173,7 @@ def test_MultiBandTilerFactory(): bands = MultiBandTilerFactory( reader=BandFileReader, path_dependency=CustomPathParams ) - assert len(bands.router.routes) == 28 + assert len(bands.router.routes) == 24 app = FastAPI() app.include_router(bands.router) @@ -981,12 +1182,20 @@ def test_MultiBandTilerFactory(): client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + + response = client.get("/docs") + assert response.status_code == 200 + response = client.get(f"/bands?directory={DATA_DIR}") assert response.status_code == 200 assert response.json() == ["B01", "B09"] # default bands - response = client.get(f"/info?directory={DATA_DIR}") + # no bands + with pytest.warns(UserWarning): + response = client.get(f"/info?directory={DATA_DIR}") assert response.json()["band_metadata"] == [["B01", {}], ["B09", {}]] response = client.get(f"/info?directory={DATA_DIR}&bands=B01") @@ -1085,6 +1294,15 @@ def test_MultiBandTilerFactory(): "percentile_98", } + response = client.get( + f"/statistics?directory={DATA_DIR}&bands=B01&bands=B09&algorithm=normalizedIndex" + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + assert len(resp) == 1 + assert "(B09 - B01) / (B09 + B01)" in resp + # POST - statistics band_feature = { "type": "FeatureCollection", @@ -1220,6 +1438,17 @@ def test_MultiBandTilerFactory(): "percentile_98", } + response = client.post( + f"/statistics?directory={DATA_DIR}&bands=B01&bands=B09&algorithm=normalizedIndex", + json=band_feature, + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "application/geo+json" + resp = response.json() + props = resp["features"][0]["properties"]["statistics"] + assert len(props) == 1 + assert "(B09 - B01) / (B09 + B01)" in props + # default bands response = client.post(f"/statistics?directory={DATA_DIR}", json=band_feature) assert response.status_code == 200 @@ -1240,6 +1469,25 @@ def test_MultiBandTilerFactory(): assert props["B01"] assert props["B09"] + # OGC Tileset + response = client.get(f"/tiles?directory={DATA_DIR}") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + assert len(resp["tilesets"]) == NB_DEFAULT_TMS + + first_tms = resp["tilesets"][0] + first_id = DEFAULT_TMS.list()[0] + assert first_id in first_tms["title"] + assert len(first_tms["links"]) == 2 # no link to the tms definition + + response = client.get(f"/tiles/WebMercatorQuad?directory={DATA_DIR}") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + resp = response.json() + # 1 Zoom level (8) + assert len(resp["tileMatrixSetLimits"]) == 1 + def test_TMSFactory(): """test TMSFactory.""" @@ -1267,8 +1515,7 @@ def test_TMSFactory(): response = client.get("/tms/tileMatrixSets/WebMercatorQuad") assert response.status_code == 200 body = response.json() - assert body["type"] == "TileMatrixSetType" - assert body["identifier"] == "WebMercatorQuad" + assert body["id"] == "WebMercatorQuad" response = client.get("/tms/tileMatrixSets/WebMercatorQua") assert response.status_code == 422 @@ -1312,14 +1559,14 @@ def must_be_bob(credentials: security.HTTPBasicCredentials = Depends(http_basic) ( [ {"path": "/bounds", "method": "GET"}, - {"path": "/tiles/{z}/{x}/{y}", "method": "GET"}, + {"path": "/tiles/{tileMatrixSetId}/{z}/{x}/{y}", "method": "GET"}, ], [Depends(must_be_bob)], ), ], router_prefix="something", ) - assert len(cog.router.routes) == 27 + assert len(cog.router.routes) == 23 app = FastAPI() app.include_router(cog.router, prefix="/something") @@ -1328,7 +1575,9 @@ def must_be_bob(credentials: security.HTTPBasicCredentials = Depends(http_basic) auth_bob = httpx.BasicAuth(username="bob", password="ILoveSponge") auth_notbob = httpx.BasicAuth(username="notbob", password="IHateSponge") - response = client.get(f"/something/tilejson.json?url={DATA_DIR}/cog.tif") + response = client.get( + f"/something/WebMercatorQuad/tilejson.json?url={DATA_DIR}/cog.tif" + ) assert response.status_code == 200 assert response.headers["content-type"] == "application/json" assert response.json()["tilejson"] @@ -1345,20 +1594,21 @@ def must_be_bob(credentials: security.HTTPBasicCredentials = Depends(http_basic) assert response.json()["detail"] == "You're not Bob" response = client.get( - f"/something/tiles/8/87/48?url={DATA_DIR}/cog.tif&rescale=0,1000", auth=auth_bob + f"/something/tiles/WebMercatorQuad/8/87/48?url={DATA_DIR}/cog.tif&rescale=0,1000", + auth=auth_bob, ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" response = client.get( - f"/something/tiles/8/87/48?url={DATA_DIR}/cog.tif&rescale=0,1000", + f"/something/tiles/WebMercatorQuad/8/87/48?url={DATA_DIR}/cog.tif&rescale=0,1000", auth=auth_notbob, ) assert response.status_code == 401 assert response.json()["detail"] == "You're not Bob" response = client.get( - f"/something/tiles/8/87/48.jpeg?url={DATA_DIR}/cog.tif&rescale=0,1000" + f"/something/tiles/WebMercatorQuad/8/87/48.jpeg?url={DATA_DIR}/cog.tif&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "image/jpeg" @@ -1373,7 +1623,9 @@ def must_be_bob(credentials: security.HTTPBasicCredentials = Depends(http_basic) app.include_router(cog.router, prefix="/something") client = TestClient(app) - response = client.get(f"/something/tilejson.json?url={DATA_DIR}/cog.tif") + response = client.get( + f"/something/WebMercatorQuad/tilejson.json?url={DATA_DIR}/cog.tif" + ) assert response.status_code == 200 assert response.headers["content-type"] == "application/json" assert response.json()["tilejson"] @@ -1400,7 +1652,8 @@ def test_TilerFactory_WithGdalEnv(): app.include_router(router) client = TestClient(app) - response = client.get(f"/info?url={DATA_DIR}/non_cog.tif") + with pytest.warns(NoOverviewWarning): + response = client.get(f"/info?url={DATA_DIR}/non_cog.tif") assert not response.json()["overviews"] router = TilerFactory( @@ -1414,7 +1667,6 @@ def test_TilerFactory_WithGdalEnv(): assert response.json()["overviews"] class ReaddirType(str, Enum): - false = "false" true = "true" empty_dir = "empty_dir" @@ -1427,14 +1679,21 @@ def gdal_env(disable_read: ReaddirType = Query(ReaddirType.false)): app.include_router(router) client = TestClient(app) - response = client.get(f"/info?url={DATA_DIR}/non_cog.tif") - assert response.json()["overviews"] + with warnings.catch_warnings(): + warnings.simplefilter("error") + response = client.get(f"/info?url={DATA_DIR}/non_cog.tif") + assert response.json()["overviews"] - response = client.get(f"/info?url={DATA_DIR}/non_cog.tif&disable_read=false") - assert response.json()["overviews"] + with warnings.catch_warnings(): + warnings.simplefilter("error") + response = client.get(f"/info?url={DATA_DIR}/non_cog.tif&disable_read=false") + assert response.json()["overviews"] - response = client.get(f"/info?url={DATA_DIR}/non_cog.tif&disable_read=empty_dir") - assert not response.json()["overviews"] + with pytest.warns(NoOverviewWarning): + response = client.get( + f"/info?url={DATA_DIR}/non_cog.tif&disable_read=empty_dir" + ) + assert not response.json()["overviews"] def test_algorithm(): @@ -1456,8 +1715,8 @@ def test_algorithm(): def test_path_param_in_prefix(): """Test path params in prefix.""" - @dataclass - class EndpointFactory(BaseTilerFactory): + @define + class EndpointFactory(BaseFactory): def register_routes(self): """register endpoints.""" @@ -1474,7 +1733,7 @@ def route1(param1: int = Path(...), param2: str = Path(...)): return {"value": param2} app = FastAPI() - endpoints = EndpointFactory(reader=Reader, router_prefix="/prefixed/{param1}") + endpoints = EndpointFactory(router_prefix="/prefixed/{param1}") app.include_router(endpoints.router, prefix="/prefixed/{param1}") client = TestClient(app) @@ -1495,9 +1754,11 @@ def test_AutoFormat_Colormap(): app.include_router(cog.router) with TestClient(app) as client: - - cmap = urlencode( - { + response = client.get( + "/preview", + params={ + "url": f"{DATA_DIR}/cog.tif", + "bidx": 1, "colormap": json.dumps( [ # ([min, max], [r, g, b, a]) @@ -1505,14 +1766,11 @@ def test_AutoFormat_Colormap(): ([2, 6000], [255, 0, 0, 255]), ([6001, 300000], [0, 255, 0, 255]), ] - ) - } + ), + }, ) - - response = client.get(f"/preview?url={DATA_DIR}/cog.tif&bidx=1&{cmap}") assert response.status_code == 200 assert response.headers["content-type"] == "image/png" - with MemoryFile(response.content) as mem: with mem.open() as dst: img = dst.read() @@ -1528,11 +1786,34 @@ def test_AutoFormat_Colormap(): def test_rescale_dependency(): """Ensure that we can set default rescale values via the rescale_dependency""" - def custom_rescale_params() -> Optional[RescaleType]: - return [(0, 100)] + @dataclass + class ImageRenderingParams(dependencies.ImageRenderingParams): + """Custom ImageParams.""" + + def __post_init__(self): + if self.rescale: + rescale_array = [] + for r in self.rescale: + parsed = tuple( + map( + float, + r.replace(" ", "") + .replace("[", "") + .replace("]", "") + .split(","), + ) + ) + assert ( + len(parsed) == 2 + ), f"Invalid rescale values: {self.rescale}, should be of form ['min,max', 'min,max'] or [[min,max], [min, max]]" + rescale_array.append(parsed) + + self.rescale = rescale_array # Noqa + else: + self.rescale = [(0, 100)] cog = TilerFactory() - cog_custom_range = TilerFactory(rescale_dependency=custom_rescale_params) + cog_custom_range = TilerFactory(render_dependency=ImageRenderingParams) app = FastAPI() app.include_router(cog.router, prefix="/cog") @@ -1540,7 +1821,7 @@ def custom_rescale_params() -> Optional[RescaleType]: with TestClient(app) as client: response = client.get( - f"/cog/tiles/8/87/48.npy?url={DATA_DIR}/cog.tif&rescale=0,1000" + f"/cog/tiles/WebMercatorQuad/8/87/48.npy?url={DATA_DIR}/cog.tif&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" @@ -1548,7 +1829,7 @@ def custom_rescale_params() -> Optional[RescaleType]: assert npy_tile.shape == (2, 256, 256) # mask + data response = client.get( - f"/cog_custom/tiles/8/87/48.npy?url={DATA_DIR}/cog.tif&rescale=0,1000" + f"/cog_custom/tiles/WebMercatorQuad/8/87/48.npy?url={DATA_DIR}/cog.tif&rescale=0,1000" ) assert response.status_code == 200 assert response.headers["content-type"] == "application/x-binary" @@ -1557,7 +1838,7 @@ def custom_rescale_params() -> Optional[RescaleType]: def test_dst_crs_option(): - """test dst-crs parameter.""" + """test dst_crs parameter.""" app = FastAPI() app.include_router(TilerFactory().router) @@ -1571,14 +1852,14 @@ def test_dst_crs_option(): 32621 ) # return the image in the original CRS - response = client.get(f"/preview.tif?url={DATA_DIR}/cog.tif&dst-crs=epsg:4326") + response = client.get(f"/preview.tif?url={DATA_DIR}/cog.tif&dst_crs=epsg:4326") meta = parse_img(response.content) assert meta["crs"] == CRS.from_epsg(4326) assert not meta["crs"] == CRS.from_epsg(32621) - # /crop endpoints + # /bbox endpoints response = client.get( - f"/crop/-56.228,72.715,-54.547,73.188.tif?url={DATA_DIR}/cog.tif" + f"/bbox/-56.228,72.715,-54.547,73.188.tif?url={DATA_DIR}/cog.tif" ) meta = parse_img(response.content) assert meta["crs"] == CRS.from_epsg( @@ -1588,20 +1869,189 @@ def test_dst_crs_option(): # Force output in epsg:32621 response = client.get( - f"/crop/-56.228,72.715,-54.547,73.188.tif?url={DATA_DIR}/cog.tif&dst-crs=epsg:32621" + f"/bbox/-56.228,72.715,-54.547,73.188.tif?url={DATA_DIR}/cog.tif&dst_crs=epsg:32621" ) meta = parse_img(response.content) assert meta["crs"] == CRS.from_epsg(32621) - # coord-crs + dst-crs + # coord_crs + dst_crs response = client.get( - f"/crop/-6259272.328324187,12015838.020930404,-6072144.264300693,12195445.265479913.tif?url={DATA_DIR}/cog.tif&coord-crs=epsg:3857" + f"/bbox/-6259272.328324187,12015838.020930404,-6072144.264300693,12195445.265479913.tif?url={DATA_DIR}/cog.tif&coord_crs=epsg:3857" ) meta = parse_img(response.content) assert meta["crs"] == CRS.from_epsg(3857) response = client.get( - f"/crop/-6259272.328324187,12015838.020930404,-6072144.264300693,12195445.265479913.tif?url={DATA_DIR}/cog.tif&coord-crs=epsg:3857&dst-crs=epsg:32621" + f"/bbox/-6259272.328324187,12015838.020930404,-6072144.264300693,12195445.265479913.tif?url={DATA_DIR}/cog.tif&coord_crs=epsg:3857&dst_crs=epsg:32621" ) meta = parse_img(response.content) assert meta["crs"] == CRS.from_epsg(32621) + + +def test_color_formula_dependency(): + """Ensure that we can set default color formulae via the color_formula_dependency""" + + @dataclass + class ImageRenderingParams(dependencies.ImageRenderingParams): + """Custom ImageParams.""" + + color_formula: Annotated[ + Optional[str], + Query( + title="Color Formula", + description="rio-color formula (info: https://github.com/mapbox/rio-color)", + ), + ] = "sigmoidal R 7 0.4" + + cog = TilerFactory() + cog_custom_color_formula = TilerFactory(render_dependency=ImageRenderingParams) + + app = FastAPI() + app.include_router(cog.router, prefix="/cog") + app.include_router(cog_custom_color_formula.router, prefix="/cog_custom") + + with TestClient(app) as client: + response = client.get( + f"/cog/tiles/WebMercatorQuad/8/87/48.npy?url={DATA_DIR}/cog.tif&color_formula=sigmoidal R 10 0.1" + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "application/x-binary" + npy_tile = numpy.load(BytesIO(response.content)) + assert npy_tile.shape == (2, 256, 256) # mask + data + + response = client.get( + f"/cog_custom/tiles/WebMercatorQuad/8/87/48.npy?url={DATA_DIR}/cog.tif" + ) + assert response.status_code == 200 + assert response.headers["content-type"] == "application/x-binary" + numpy.load(BytesIO(response.content)) + assert npy_tile.shape == (2, 256, 256) # mask + data + + +def test_colormap_factory(): + """Test ColorMapFactory endpoint.""" + # Register custom colormaps + cmaps = default_cmap.register( + { + "cust": {0: (0, 0, 0, 255), 1: (255, 0, 0, 255), 255: (255, 255, 0, 255)}, + "negative": { + -100: (0, 0, 0, 255), + 1: (255, 0, 0, 255), + 255: (255, 255, 0, 255), + }, + "seq": [ + ((1, 2), (255, 0, 0, 255)), + ((2, 3), (255, 240, 255, 255)), + ], + } + ) + + cmaps = ColorMapFactory(supported_colormaps=cmaps) + + app = FastAPI() + app.include_router(cmaps.router) + client = TestClient(app) + + response = client.get("/colorMaps") + assert response.status_code == 200 + assert "cust" in response.json()["colorMaps"] + assert "negative" in response.json()["colorMaps"] + assert "seq" in response.json()["colorMaps"] + assert "viridis" in response.json()["colorMaps"] + + response = client.get("/colorMaps/viridis") + assert response.status_code == 200 + + response = client.get("/colorMaps/cust") + assert response.status_code == 200 + + response = client.get("/colorMaps/negative") + assert response.status_code == 200 + + response = client.get("/colorMaps/seq") + assert response.status_code == 200 + + response = client.get("/colorMaps/yo") + assert response.status_code == 422 + + response = client.get("/colorMaps/viridis", params={"format": "png"}) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["dtype"] == "uint8" + assert meta["count"] == 4 + assert meta["width"] == 256 + assert meta["height"] == 20 + + response = client.get( + "/colorMaps/viridis", params={"format": "png", "orientation": "vertical"} + ) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["dtype"] == "uint8" + assert meta["count"] == 4 + assert meta["width"] == 20 + assert meta["height"] == 256 + + response = client.get( + "/colorMaps/viridis", params={"format": "png", "width": 1000, "height": 100} + ) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["dtype"] == "uint8" + assert meta["count"] == 4 + assert meta["width"] == 1000 + assert meta["height"] == 100 + + response = client.get("/colorMaps/cust", params={"format": "png"}) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["dtype"] == "uint8" + assert meta["count"] == 4 + assert meta["width"] == 256 + assert meta["height"] == 20 + + response = client.get( + "/colorMaps/cust", params={"format": "png", "orientation": "vertical"} + ) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["dtype"] == "uint8" + assert meta["count"] == 4 + assert meta["width"] == 20 + assert meta["height"] == 256 + + response = client.get("/colorMaps/negative", params={"format": "png"}) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["dtype"] == "uint8" + assert meta["count"] == 4 + assert meta["width"] == 256 + assert meta["height"] == 20 + + response = client.get( + "/colorMaps/negative", params={"format": "png", "orientation": "vertical"} + ) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["dtype"] == "uint8" + assert meta["count"] == 4 + assert meta["width"] == 20 + assert meta["height"] == 256 + + response = client.get("/colorMaps/seq", params={"format": "png"}) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["dtype"] == "uint8" + assert meta["count"] == 4 + assert meta["width"] == 256 + assert meta["height"] == 20 + + response = client.get( + "/colorMaps/seq", params={"format": "png", "orientation": "vertical"} + ) + assert response.status_code == 200 + meta = parse_img(response.content) + assert meta["dtype"] == "uint8" + assert meta["count"] == 4 + assert meta["width"] == 20 + assert meta["height"] == 256 diff --git a/src/titiler/core/tests/test_logger_middleware.py b/src/titiler/core/tests/test_logger_middleware.py index 9c298f5c5..7987d6f0b 100644 --- a/src/titiler/core/tests/test_logger_middleware.py +++ b/src/titiler/core/tests/test_logger_middleware.py @@ -1,12 +1,17 @@ -"""Test titiler.core.middleware.TotalTimeMiddleware.""" +"""Test titiler.core.middleware.LoggerMiddleware.""" -from fastapi import FastAPI +import json +import logging +from logging import config + +import pytest +from fastapi import FastAPI, Path from starlette.testclient import TestClient from titiler.core.middleware import LoggerMiddleware -def test_timing_middleware_exclude(caplog): +def test_logger_middleware(caplog): """Create App.""" app = FastAPI() @@ -15,18 +20,103 @@ async def route1(): """route1.""" return "Yo" - app.add_middleware(LoggerMiddleware, querystrings=True, headers=True) + @app.get("/route2/{value}") + async def route2(value: str = Path()): + """route2.""" + return value + + @app.get("/route3/{value}") + async def route3(value: str = Path()): + """route3.""" + raise Exception("something went wrong") + + app.add_middleware(LoggerMiddleware) + + config.dictConfig( + { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "request": { + "format": ( + "%(asctime)s - %(levelname)s - %(name)s - %(message)s " + + json.dumps( + { + k: f"%({k})s" + for k in [ + "http.method", + "http.referer", + "http.origin", + "http.route", + "http.path", + "titiler.path_params", + "titiler.query_params", + "http.request.header.", + ] + } + ) + ), + }, + }, + "handlers": { + "console_request": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "request", + "stream": "ext://sys.stdout", + }, + }, + "loggers": { + "titiler.requests": { + "level": "INFO", + "handlers": ["console_request"], + "propagate": True, + }, + }, + } + ) with TestClient(app) as client: - caplog.clear() - client.get("/route1") - assert len([rec.message for rec in caplog.records]) == 2 - - caplog.clear() - client.get("/route1", params={"hey": "yo"}) - assert len([rec.message for rec in caplog.records]) == 3 - - caplog.clear() - client.get("/route1", params={"hey": "yo"}, headers={"accept-encoding": "gzip"}) - h = caplog.records[2].message - assert "'accept-encoding': 'gzip'" in h + with caplog.at_level(logging.INFO, logger="titiler.requests"): + caplog.clear() + client.get("/route1") + log = caplog.records[0] + assert log.name == "titiler.requests" + assert log.levelname == "INFO" + assert log.message == "Request received: /route1 GET" + assert hasattr(log, "titiler.query_params") + assert getattr(log, "http.route") == "/route1" + + caplog.clear() + client.get("/route1", params={"hey": "yo"}) + log = caplog.records[0] + assert log.message == "Request received: /route1 GET" + assert getattr(log, "titiler.query_params") == {"hey": "yo"} + + caplog.clear() + client.get( + "/route1", params={"hey": "yo"}, headers={"accept-encoding": "gzip"} + ) + log = caplog.records[0] + assert log.message == "Request received: /route1 GET" + assert getattr(log, "titiler.query_params") == {"hey": "yo"} + assert getattr(log, "http.request.header.accept-encoding") == "gzip" + + caplog.clear() + client.get("/route2/val") + log = caplog.records[0] + assert log.name == "titiler.requests" + assert log.levelname == "INFO" + assert log.message == "Request received: /route2/val GET" + assert hasattr(log, "titiler.query_params") + assert getattr(log, "http.route") == "/route2/{value}" + + caplog.clear() + with pytest.raises(Exception): # noqa: B017 + client.get("/route3/val") + log = caplog.records[0] + assert log.name == "titiler.requests" + assert log.levelname == "INFO" + assert log.message == "Request received: /route3/val GET" + assert hasattr(log, "titiler.query_params") + assert log.route == "/route3/{value}" diff --git a/src/titiler/core/tests/test_rendering.py b/src/titiler/core/tests/test_rendering.py new file mode 100644 index 000000000..1241a5eae --- /dev/null +++ b/src/titiler/core/tests/test_rendering.py @@ -0,0 +1,105 @@ +"""test titiler rendering function.""" + +import warnings + +import numpy +import pytest +from rasterio.io import MemoryFile +from rio_tiler.errors import InvalidDatatypeWarning +from rio_tiler.models import ImageData + +from titiler.core.resources.enums import ImageType +from titiler.core.utils import render_image + + +def test_rendering(): + """test rendering.""" + im = ImageData(numpy.zeros((1, 256, 256), dtype="uint8")) + + # Should render as JPEG + content, media = render_image(im) + assert media == "image/jpeg" + with MemoryFile(content) as mem: + with mem.open() as dst: + assert dst.profile["driver"] == "JPEG" + assert dst.count == 1 + assert dst.width == 256 + assert dst.height == 256 + arr = dst.read() + assert numpy.unique(arr).tolist() == [0] + + # Should render as PNG + content, media = render_image(im, output_format=ImageType.png) + assert media == "image/png" + with MemoryFile(content) as mem: + with mem.open() as dst: + assert dst.profile["driver"] == "PNG" + assert dst.count == 2 + arr = dst.read() + assert numpy.unique(arr[0]).tolist() == [0] + + with pytest.warns(InvalidDatatypeWarning): + _, media = render_image( + ImageData(numpy.zeros((1, 256, 256), dtype="uint16")), + output_format=ImageType.jpeg, + ) + assert media == "image/jpeg" + + with pytest.warns(InvalidDatatypeWarning): + _, media = render_image( + ImageData(numpy.zeros((1, 256, 256), dtype="float32")), + output_format=ImageType.png, + ) + assert media == "image/png" + + with pytest.warns(InvalidDatatypeWarning): + _, media = render_image( + ImageData(numpy.zeros((1, 256, 256), dtype="float32")), + output_format=ImageType.jp2, + ) + assert media == "image/jp2" + + # Make sure that we do not rescale uint16 data when there is a colormap + # Because the colormap will result in data between 0 and 255 it should be of type uint8 + with warnings.catch_warnings(): + warnings.simplefilter("error") + cm = {1: (0, 0, 0, 255), 1000: (255, 255, 255, 255)} + d = numpy.zeros((1, 256, 256), dtype="float32") + 1 + d[0, 0:10, 0:10] = 1000 + content, media = render_image( + ImageData(d), + output_format=ImageType.jpeg, + colormap=cm, + ) + assert media == "image/jpeg" + + with MemoryFile(content) as mem: + with mem.open() as dst: + assert dst.count == 3 + assert dst.dtypes == ("uint8", "uint8", "uint8") + assert dst.read()[:, 0, 0].tolist() == [255, 255, 255] + assert dst.read()[:, 11, 11].tolist() == [0, 0, 0] + + # Partial alpha values + cm = { + 1: (0, 0, 0, 0), + 500: (100, 100, 100, 50), + 1000: (255, 255, 255, 255), + } + d = numpy.ma.zeros((1, 256, 256), dtype="float32") + 1 + d[0, 0:10, 0:10] = 500 + d[0, 10:20, 10:20] = 1000 + content, media = render_image( + ImageData(d), + output_format=ImageType.png, + colormap=cm, + ) + assert media == "image/png" + + with MemoryFile(content) as mem: + with mem.open() as dst: + assert dst.count == 4 + assert dst.dtypes == ("uint8", "uint8", "uint8", "uint8") + assert dst.read()[:, 0, 0].tolist() == [100, 100, 100, 50] + assert dst.read()[:, 11, 11].tolist() == [255, 255, 255, 255] + assert dst.read()[:, 30, 30].tolist() == [0, 0, 0, 0] diff --git a/src/titiler/core/tests/test_telemetry.py b/src/titiler/core/tests/test_telemetry.py new file mode 100644 index 000000000..8b3574a59 --- /dev/null +++ b/src/titiler/core/tests/test_telemetry.py @@ -0,0 +1,96 @@ +"""telemetry tests""" + +import os + +import pytest +from fastapi import FastAPI +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter +from opentelemetry.trace import StatusCode +from starlette.testclient import TestClient + +from titiler.core import telemetry +from titiler.core.factory import TilerFactory + +TEST_URL = f"file://{os.path.join(os.path.dirname(__file__), 'fixtures', 'cog.tif')}" +TEST_Z, TEST_X, TEST_Y = 8, 84, 47 + + +@pytest.fixture +def telemetry_disabled(monkeypatch): + """Fixture to simulate OTel being disabled by monkeypatching the tracer.""" + monkeypatch.setattr("titiler.core.telemetry.tracer", None) + monkeypatch.setattr("titiler.core.telemetry.factory_trace.decorator_enabled", False) + + +@pytest.fixture +def memory_exporter(): + """Fixture to configure an in-memory exporter for capturing spans.""" + tracer_provider = TracerProvider() + exporter = InMemorySpanExporter() + processor = SimpleSpanProcessor(exporter) + tracer_provider.add_span_processor(processor) + + original_provider = trace.get_tracer_provider() + trace.set_tracer_provider(tracer_provider) + + yield exporter + + exporter.shutdown() + trace.set_tracer_provider(original_provider) + + +def test_tracing_disabled_noop(telemetry_disabled): + """Test that the application works correctly when OTel is not installed.""" + assert not telemetry.tracer + assert not telemetry.factory_trace.decorator_enabled + + app = FastAPI() + tiler = TilerFactory(router_prefix="cog") + app.include_router(tiler.router, prefix="/cog") + client = TestClient(app) + + response = client.get( + f"/cog/tiles/WebMercatorQuad/{TEST_Z}/{TEST_X}/{TEST_Y}.png", + params={ + "url": TEST_URL, + }, + ) + assert response.status_code == 200 + + +def test_tracing_enabled_but_not_available_warning(telemetry_disabled): + """Test that enabling telemetry without the decorator enabled emits a warning.""" + assert not telemetry.tracer + assert not telemetry.factory_trace.decorator_enabled + + with pytest.warns(match="tracing is not available"): + _ = TilerFactory(router_prefix="cog", enable_telemetry=True) + + +def test_tracing_enabled_success_path(memory_exporter): + """Test that spans are correctly created on a successful request.""" + assert telemetry.tracer + assert telemetry.factory_trace.decorator_enabled + + app = FastAPI() + tiler = TilerFactory(router_prefix="cog", enable_telemetry=True) + app.include_router(tiler.router, prefix="/cog") + client = TestClient(app) + + response = client.get( + f"/cog/tiles/WebMercatorQuad/{TEST_Z}/{TEST_X}/{TEST_Y}.png", + params={ + "url": TEST_URL, + }, + ) + assert response.status_code == 200 + + finished_spans = memory_exporter.get_finished_spans() + assert len(finished_spans) == 1 + + span = next(filter(lambda x: x.name == "TilerFactory.tile", finished_spans), None) + assert span + assert span.status.status_code == StatusCode.OK diff --git a/src/titiler/core/tests/test_utils.py b/src/titiler/core/tests/test_utils.py new file mode 100644 index 000000000..8084c7dc5 --- /dev/null +++ b/src/titiler/core/tests/test_utils.py @@ -0,0 +1,180 @@ +"""Test utils.""" + +import pytest + +from titiler.core.dependencies import AssetsBidxExprParams, BidxParams +from titiler.core.resources.enums import MediaType +from titiler.core.utils import ( + accept_media_type, + check_query_params, + deserialize_query_params, + extract_query_params, + get_dependency_query_params, +) + + +def test_get_dependency_params(): + """Test dependency filtering from query params.""" + + # invalid + values, err = get_dependency_query_params( + dependency=BidxParams, params={"bidx": ["invalid type"]} + ) + assert values == {} + assert err + assert err == [ + { + "input": "invalid type", + "loc": ( + "query", + "bidx", + 0, + ), + "msg": "Input should be a valid integer, unable to parse string as an integer", + "type": "int_parsing", + }, + ] + + # not in dep + values, err = get_dependency_query_params( + dependency=BidxParams, params={"not_in_dep": "no error, no value"} + ) + assert values == {"indexes": None} + assert not err + + # valid + values, err = get_dependency_query_params( + dependency=BidxParams, params={"bidx": [1, 2, 3]} + ) + assert values == {"indexes": [1, 2, 3]} + assert not err + + # valid and not in dep + values, err = get_dependency_query_params( + dependency=BidxParams, + params={"bidx": [1, 2, 3], "other param": "to be filtered out"}, + ) + assert values == {"indexes": [1, 2, 3]} + assert not err + + +def test_deserialize_query_params(): + """Test deserialize_query_params.""" + # invalid + res, err = deserialize_query_params( + dependency=BidxParams, params={"bidx": ["invalid type"]} + ) + assert res == BidxParams(indexes=None) + assert err + + # valid + res, err = deserialize_query_params( + dependency=BidxParams, params={"not_in_dep": "no error, no value", "bidx": [1]} + ) + assert res == BidxParams(indexes=[1]) + assert not err + + +def test_extract_query_params(): + """Test extract_query_params.""" + # invalid + qs, err = extract_query_params( + dependencies=[BidxParams], + params={"bidx": ["invalid type"]}, + ) + assert qs == {} + assert len(err) + + qs, err = extract_query_params( + dependencies=[BidxParams], + params={"bidx": [1]}, + ) + assert qs == {"indexes": [1]} + assert len(err) == 0 + + qs, err = extract_query_params( + dependencies=[BidxParams], + params={"bidx": 1}, + ) + assert qs == {"indexes": [1]} + assert len(err) == 0 + + qs, err = extract_query_params( + dependencies=[BidxParams], + params={"not_in_dep": "no error, no value", "bidx": [1]}, + ) + assert qs == {"indexes": [1]} + assert len(err) == 0 + + +def test_check_query_params(): + """Test check_query_params.""" + # invalid bidx value + assert ( + check_query_params( + dependencies=[BidxParams], + params={"bidx": ["invalid type"]}, + ) + is False + ) + + # assets is required + assert ( + check_query_params( + dependencies=[AssetsBidxExprParams], + params={}, + ) + is False + ) + + assert ( + check_query_params( + dependencies=[AssetsBidxExprParams, BidxParams], + params={"assets": "yo", "bidx": 1}, + ) + is True + ) + + +@pytest.mark.parametrize( + "media,accept,expected", + [ + ([MediaType.html], "text/html, application/json;q=0.8", MediaType.html), + ( + [MediaType.html, MediaType.json], + "application/json, text/html;q=0.8", + MediaType.json, + ), + ([MediaType.xml], "application/json, text/html;q=0.8", None), + ([MediaType.json], "", None), + ( + [MediaType.json, MediaType.html], + "application/json;q=1.0, text/html;q=0.8", + MediaType.json, + ), + ( + [MediaType.json, MediaType.html], + "application/json;q=1.0, text/html;q=1.0", + MediaType.json, + ), + ( + [MediaType.html, MediaType.json], + "application/json;q=1.0, text/html;q=1.0", + MediaType.html, + ), + ([MediaType.html, MediaType.json], "*;q=1.0", MediaType.html), + ( + [MediaType.json, MediaType.html], + "application/json;q=aaa, text/html", + MediaType.html, + ), + ( + [MediaType.json, MediaType.html], + "application/json;q=0.0, text/html", + MediaType.html, + ), + ], +) +def test_accept_media_type(media, accept, expected): + """test MetadataOutputType dependency.""" + assert accept_media_type(accept, media) == expected diff --git a/src/titiler/core/titiler/core/__init__.py b/src/titiler/core/titiler/core/__init__.py index f26d8963f..62bb42831 100644 --- a/src/titiler/core/titiler/core/__init__.py +++ b/src/titiler/core/titiler/core/__init__.py @@ -1,11 +1,14 @@ """titiler.core""" -__version__ = "0.11.7" +__version__ = "0.22.4" from . import dependencies, errors, factory, routing # noqa from .factory import ( # noqa - BaseTilerFactory, + AlgorithmFactory, + BaseFactory, + ColorMapFactory, MultiBandTilerFactory, MultiBaseTilerFactory, TilerFactory, + TMSFactory, ) diff --git a/src/titiler/core/titiler/core/algorithm/__init__.py b/src/titiler/core/titiler/core/algorithm/__init__.py index b77327384..883cf8561 100644 --- a/src/titiler/core/titiler/core/algorithm/__init__.py +++ b/src/titiler/core/titiler/core/algorithm/__init__.py @@ -7,17 +7,31 @@ import attr from fastapi import HTTPException, Query from pydantic import ValidationError +from typing_extensions import Annotated -from titiler.core.algorithm.base import AlgorithmMetadata, BaseAlgorithm # noqa -from titiler.core.algorithm.dem import Contours, HillShade, TerrainRGB, Terrarium +from titiler.core.algorithm.base import AlgorithmMetadata # noqa +from titiler.core.algorithm.base import BaseAlgorithm +from titiler.core.algorithm.dem import Contours, HillShade, Slope, TerrainRGB, Terrarium from titiler.core.algorithm.index import NormalizedIndex +from titiler.core.algorithm.math import _Max, _Mean, _Median, _Min, _Std, _Var +from titiler.core.algorithm.ops import CastToInt, Ceil, Floor default_algorithms: Dict[str, Type[BaseAlgorithm]] = { "hillshade": HillShade, + "slope": Slope, "contours": Contours, "normalizedIndex": NormalizedIndex, "terrarium": Terrarium, "terrainrgb": TerrainRGB, + "cast": CastToInt, + "ceil": Ceil, + "floor": Floor, + "min": _Min, + "max": _Max, + "median": _Median, + "mean": _Mean, + "std": _Std, + "var": _Var, } @@ -55,10 +69,14 @@ def dependency(self): """FastAPI PostProcess dependency.""" def post_process( - algorithm: Literal[tuple(self.data.keys())] = Query( - None, description="Algorithm name" - ), - algorithm_params: str = Query(None, description="Algorithm parameter"), + algorithm: Annotated[ + Literal[tuple(self.data.keys())], + Query(description="Algorithm name"), + ] = None, + algorithm_params: Annotated[ + Optional[str], + Query(description="Algorithm parameter"), + ] = None, ) -> Optional[BaseAlgorithm]: """Data Post-Processing options.""" kwargs = json.loads(algorithm_params) if algorithm_params else {} diff --git a/src/titiler/core/titiler/core/algorithm/base.py b/src/titiler/core/titiler/core/algorithm/base.py index 5eb6bdf3b..33ee9c370 100644 --- a/src/titiler/core/titiler/core/algorithm/base.py +++ b/src/titiler/core/titiler/core/algorithm/base.py @@ -15,26 +15,26 @@ class BaseAlgorithm(BaseModel, metaclass=abc.ABCMeta): """ # metadata - input_nbands: Optional[int] - output_nbands: Optional[int] - output_dtype: Optional[str] - output_min: Optional[Sequence] - output_max: Optional[Sequence] + input_nbands: Optional[int] = None + output_nbands: Optional[int] = None + output_dtype: Optional[str] = None + output_min: Optional[Sequence] = None + output_max: Optional[Sequence] = None + + model_config = {"extra": "allow"} @abc.abstractmethod def __call__(self, img: ImageData) -> ImageData: """Apply algorithm""" ... - class Config: - """Config for model.""" - - extra = "allow" - class AlgorithmMetadata(BaseModel): """Algorithm metadata.""" + title: Optional[str] = None + description: Optional[str] = None + inputs: Dict outputs: Dict parameters: Dict diff --git a/src/titiler/core/titiler/core/algorithm/dem.py b/src/titiler/core/titiler/core/algorithm/dem.py index 858d538d2..571315af5 100644 --- a/src/titiler/core/titiler/core/algorithm/dem.py +++ b/src/titiler/core/titiler/core/algorithm/dem.py @@ -1,6 +1,9 @@ """titiler.core.algorithm DEM.""" +from typing import Optional + import numpy +from pydantic import Field from rasterio import windows from rio_tiler.colormap import apply_cmap, cmap from rio_tiler.models import ImageData @@ -8,14 +11,20 @@ from titiler.core.algorithm.base import BaseAlgorithm +__all__ = ["HillShade", "Slope", "Contours", "Terrarium", "TerrainRGB"] + class HillShade(BaseAlgorithm): """Hillshade.""" + title: str = "Hillshade" + description: str = "Create hillshade from DEM dataset." + # parameters - azimuth: int = 90 - angle_altitude: float = 90 - buffer: int = 3 + azimuth: int = Field(45, ge=0, le=360) + angle_altitude: float = Field(45.0, ge=-90.0, le=90.0) + buffer: int = Field(3, ge=0, le=99) + z_exaggeration: float = Field(1.0, ge=1e-6, le=1e6) # metadata input_nbands: int = 1 @@ -24,41 +33,90 @@ class HillShade(BaseAlgorithm): def __call__(self, img: ImageData) -> ImageData: """Create hillshade from DEM dataset.""" - data = img.data[0] - mask = img.mask - bounds = img.bounds - - x, y = numpy.gradient(data) - + x, y = numpy.gradient(img.array[0]) + x *= self.z_exaggeration + y *= self.z_exaggeration slope = numpy.pi / 2.0 - numpy.arctan(numpy.sqrt(x * x + y * y)) aspect = numpy.arctan2(-x, y) - azimuthrad = self.azimuth * numpy.pi / 180.0 + azimuth = 360.0 - self.azimuth + azimuthrad = azimuth * numpy.pi / 180.0 altituderad = self.angle_altitude * numpy.pi / 180.0 shaded = numpy.sin(altituderad) * numpy.sin(slope) + numpy.cos( altituderad ) * numpy.cos(slope) * numpy.cos(azimuthrad - aspect) - hillshade_array = 255 * (shaded + 1) / 2 + data = 255 * (shaded + 1) / 2 + data[data < 0] = 0 # set hillshade values to min of 0. + + bounds = img.bounds + if self.buffer: + data = data[self.buffer : -self.buffer, self.buffer : -self.buffer] + + window = windows.Window( + col_off=self.buffer, + row_off=self.buffer, + width=data.shape[1], + height=data.shape[0], + ) + bounds = windows.bounds(window, img.transform) + + return ImageData( + data.astype(self.output_dtype), + assets=img.assets, + crs=img.crs, + bounds=bounds, + band_names=["hillshade"], + ) + - data = numpy.expand_dims(hillshade_array, axis=0).astype(dtype=numpy.uint8) +class Slope(BaseAlgorithm): + """Slope calculation.""" + title: str = "Slope" + description: str = "Calculate degrees of slope from DEM dataset." + + # parameters + buffer: int = Field(3, ge=0, le=99, description="Buffer size for edge effects") + z_exaggeration: float = Field(1.0, ge=1e-6, le=1e6) + + # metadata + input_nbands: int = 1 + output_nbands: int = 1 + output_dtype: str = "float32" + output_min: float = 0 + output_max: float = 90 + + def __call__(self, img: ImageData) -> ImageData: + """Calculate degrees slope from DEM dataset.""" + # Get the pixel size from the transform + pixel_size_x = abs(img.transform[0]) + pixel_size_y = abs(img.transform[4]) + + x, y = numpy.gradient(img.array[0]) + x *= self.z_exaggeration + y *= self.z_exaggeration + dx = x / pixel_size_x + dy = y / pixel_size_y + + slope = numpy.arctan(numpy.sqrt(dx * dx + dy * dy)) * (180 / numpy.pi) + + bounds = img.bounds if self.buffer: - data = data[:, self.buffer : -self.buffer, self.buffer : -self.buffer] - mask = mask[self.buffer : -self.buffer, self.buffer : -self.buffer] - # image bounds without buffer + slope = slope[self.buffer : -self.buffer, self.buffer : -self.buffer] + window = windows.Window( col_off=self.buffer, row_off=self.buffer, - width=mask.shape[1], - height=mask.shape[0], + width=slope.shape[1], + height=slope.shape[0], ) bounds = windows.bounds(window, img.transform) return ImageData( - data, - mask, + slope.astype(self.output_dtype), assets=img.assets, crs=img.crs, bounds=bounds, + band_names=["slope"], ) @@ -68,11 +126,14 @@ class Contours(BaseAlgorithm): Original idea from https://custom-scripts.sentinel-hub.com/dem/contour-lines/ """ + title: str = "Contours" + description: str = "Create contours from DEM dataset." + # parameters - increment: int = 35 - thickness: int = 1 - minz: int = -12000 - maxz: int = 8000 + increment: int = Field(35, ge=0, le=999) + thickness: int = Field(1, ge=0, le=10) + minz: int = Field(-12000, ge=-99999, le=99999) + maxz: int = Field(8000, ge=-99999, le=99999) # metadata input_nbands: int = 1 @@ -81,18 +142,22 @@ class Contours(BaseAlgorithm): def __call__(self, img: ImageData) -> ImageData: """Add contours.""" - data = img.data + data = img.data.astype("float64") # Apply rescaling for minz,maxz to 1->255 and apply Terrain colormap - arr = linear_rescale(data, (self.minz, self.maxz), (1, 255)).astype("uint8") + arr = linear_rescale(data, (self.minz, self.maxz), (1, 255)).astype( + self.output_dtype + ) arr, _ = apply_cmap(arr, cmap.get("terrain")) # set black (0) for contour lines arr = numpy.where(data % self.increment < self.thickness, 0, arr) + data = numpy.ma.MaskedArray(arr) + data.mask = ~img.mask + return ImageData( - arr, - img.mask, + data, assets=img.assets, crs=img.crs, bounds=img.bounds, @@ -102,6 +167,10 @@ def __call__(self, img: ImageData) -> ImageData: class Terrarium(BaseAlgorithm): """Encode DEM into RGB (Mapzen Terrarium).""" + title: str = "Terrarium" + description: str = "Encode DEM into RGB (Mapzen Terrarium)." + nodata_height: Optional[float] = Field(None, ge=-99999.0, le=99999.0) + # metadata input_nbands: int = 1 output_nbands: int = 3 @@ -109,15 +178,17 @@ class Terrarium(BaseAlgorithm): def __call__(self, img: ImageData) -> ImageData: """Encode DEM into RGB.""" - data = numpy.clip(img.data[0] + 32768.0, 0.0, 65535.0) + data = numpy.clip(img.array[0] + 32768.0, 0.0, 65535.0) + if self.nodata_height is not None: + data[img.array.mask[0]] = numpy.clip( + self.nodata_height + 32768.0, 0.0, 65535.0 + ) r = data / 256 g = data % 256 b = (data * 256) % 256 - arr = numpy.stack([r, g, b]).astype(numpy.uint8) return ImageData( - arr, - img.mask, + numpy.ma.stack([r, g, b]).astype(self.output_dtype), assets=img.assets, crs=img.crs, bounds=img.bounds, @@ -127,9 +198,13 @@ def __call__(self, img: ImageData) -> ImageData: class TerrainRGB(BaseAlgorithm): """Encode DEM into RGB (Mapbox Terrain RGB).""" + title: str = "TerrainRGB" + description: str = "Encode DEM into RGB (Mapbox Terrain RGB)." + # parameters - interval: float = 0.1 - baseval: float = -10000.0 + interval: float = Field(0.1, ge=0.0, le=1.0) + baseval: float = Field(-10000.0, ge=-99999.0, le=99999.0) + nodata_height: Optional[float] = Field(None, ge=-99999.0, le=99999.0) # metadata input_nbands: int = 1 @@ -153,27 +228,28 @@ def _range_check(datarange): round_digits = 0 - data = img.data[0].astype(numpy.float64) + data = img.array[0].astype(numpy.float64) data -= self.baseval data /= self.interval data = numpy.around(data / 2**round_digits) * 2**round_digits - rows, cols = data.shape datarange = data.max() - data.min() if _range_check(datarange): - raise ValueError("Data of {} larger than 256 ** 3".format(datarange)) + raise ValueError(f"Data of {datarange} larger than 256 ** 3") + + if self.nodata_height is not None: + data[img.array.mask[0]] = ( + self.nodata_height - self.baseval + ) / self.interval - rgb = numpy.zeros((3, rows, cols), dtype=numpy.uint8) - rgb[2] = ((data / 256) - (data // 256)) * 256 - rgb[1] = (((data // 256) / 256) - ((data // 256) // 256)) * 256 - rgb[0] = ( - (((data // 256) // 256) / 256) - (((data // 256) // 256) // 256) - ) * 256 + data_int32 = data.astype(numpy.int32) + b = (data_int32) & 0xFF + g = (data_int32 >> 8) & 0xFF + r = (data_int32 >> 16) & 0xFF return ImageData( - rgb, - img.mask, + numpy.ma.stack([r, g, b]).astype(self.output_dtype), assets=img.assets, crs=img.crs, bounds=img.bounds, diff --git a/src/titiler/core/titiler/core/algorithm/index.py b/src/titiler/core/titiler/core/algorithm/index.py index 1e28e0f45..7fef4bbce 100644 --- a/src/titiler/core/titiler/core/algorithm/index.py +++ b/src/titiler/core/titiler/core/algorithm/index.py @@ -7,10 +7,15 @@ from titiler.core.algorithm.base import BaseAlgorithm +__all__ = ["NormalizedIndex"] + class NormalizedIndex(BaseAlgorithm): """Normalized Difference Index.""" + title: str = "Normalized Difference Index" + description: str = "Compute normalized difference index from two bands." + # metadata input_nbands: int = 2 output_nbands: int = 1 @@ -20,18 +25,14 @@ class NormalizedIndex(BaseAlgorithm): def __call__(self, img: ImageData) -> ImageData: """Normalized difference.""" - b1 = img.data[0].astype("float32") - b2 = img.data[1].astype("float32") - - arr = numpy.where(img.mask, (b2 - b1) / (b2 + b1), 0) - - # ImageData only accept image in form of (count, height, width) - arr = numpy.expand_dims(arr, axis=0).astype(self.output_dtype) - + b1 = img.array[0].astype("float32") + b2 = img.array[1].astype("float32") + arr = numpy.ma.MaskedArray((b2 - b1) / (b2 + b1), dtype=self.output_dtype) + bnames = img.band_names return ImageData( arr, - img.mask, assets=img.assets, crs=img.crs, bounds=img.bounds, + band_names=[f"({bnames[1]} - {bnames[0]}) / ({bnames[1]} + {bnames[0]})"], ) diff --git a/src/titiler/core/titiler/core/algorithm/math.py b/src/titiler/core/titiler/core/algorithm/math.py new file mode 100644 index 000000000..5a6fd722c --- /dev/null +++ b/src/titiler/core/titiler/core/algorithm/math.py @@ -0,0 +1,138 @@ +"""titiler.core.algorithm Math.""" + +import numpy +from rio_tiler.models import ImageData + +from titiler.core.algorithm.base import BaseAlgorithm + +__all__ = ["_Min", "_Max", "_Median", "_Mean", "_Std", "_Var"] + + +class _Min(BaseAlgorithm): + """Return Min values along the `bands` axis.""" + + title: str = "Min" + description: str = "Return Min values along the `bands` axis." + + output_nbands: int = 1 + + def __call__(self, img: ImageData) -> ImageData: + """Return Min.""" + return ImageData( + numpy.ma.min(img.array, axis=0, keepdims=True), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_names=["min"], + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) + + +class _Max(BaseAlgorithm): + """Return Max values along the `bands` axis.""" + + title: str = "Max" + description: str = "Return Max values along the `bands` axis." + + output_nbands: int = 1 + + def __call__(self, img: ImageData) -> ImageData: + """Return Max.""" + return ImageData( + numpy.ma.max(img.array, axis=0, keepdims=True), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_names=["max"], + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) + + +class _Median(BaseAlgorithm): + """Return Median values along the `bands` axis.""" + + title: str = "Median" + description: str = "Return Median values along the `bands` axis." + + output_nbands: int = 1 + output_dtype: str = "float64" + + def __call__(self, img: ImageData) -> ImageData: + """Return Median.""" + return ImageData( + numpy.ma.median(img.array, axis=0, keepdims=True), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_names=["median"], + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) + + +class _Mean(BaseAlgorithm): + """Return Mean values along the `bands` axis.""" + + title: str = "Mean" + description: str = "Return Mean values." + + output_nbands: int = 1 + output_dtype: str = "float64" + + def __call__(self, img: ImageData) -> ImageData: + """Return Mean.""" + return ImageData( + numpy.ma.mean(img.array, axis=0, keepdims=True), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_names=["mean"], + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) + + +class _Std(BaseAlgorithm): + """Return the standard deviation along the `bands` axis.""" + + title: str = "Standard deviation" + description: str = "Return the Standard Deviation along the `bands` axis." + + output_nbands: int = 1 + output_dtype: str = "float64" + + def __call__(self, img: ImageData) -> ImageData: + """Return Stddev.""" + return ImageData( + numpy.ma.std(img.array, axis=0, keepdims=True, ddof=1), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_names=["std"], + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) + + +class _Var(BaseAlgorithm): + """Return Variance values along the `bands` axis.""" + + title: str = "Variance" + description: str = "Return Variance along the `bands` axis." + + output_nbands: int = 1 + output_dtype: str = "float64" + + def __call__(self, img: ImageData) -> ImageData: + """Return Variance.""" + return ImageData( + numpy.ma.var(img.array, axis=0, keepdims=True, ddof=1), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_names=["var"], + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) diff --git a/src/titiler/core/titiler/core/algorithm/ops.py b/src/titiler/core/titiler/core/algorithm/ops.py new file mode 100644 index 000000000..250ed6d11 --- /dev/null +++ b/src/titiler/core/titiler/core/algorithm/ops.py @@ -0,0 +1,82 @@ +"""titiler.core.algorithm Ops.""" + +from typing import Sequence + +import numpy +from rio_tiler.models import ImageData + +from titiler.core.algorithm.base import BaseAlgorithm + +__all__ = ["CastToInt", "Ceil", "Floor"] + + +class CastToInt(BaseAlgorithm): + """Cast data to Integer.""" + + title: str = "Cast data to Integer" + description: str = "Cast data to Integer." + + # metadata + output_dtype: str = "uint8" + output_min: Sequence[int] = [0] + output_max: Sequence[int] = [255] + + def __call__(self, img: ImageData) -> ImageData: + """Cast Data.""" + return ImageData( + img.array.astype("uint8"), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_names=img.band_names, + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) + + +class Ceil(BaseAlgorithm): + """Round data to the smallest integer.""" + + title: str = "Round data to the smallest integer" + description: str = "Round data to the smallest integer." + + # metadata + output_dtype: str = "uint8" + output_min: Sequence[int] = [0] + output_max: Sequence[int] = [255] + + def __call__(self, img: ImageData) -> ImageData: + """Cast Data.""" + return ImageData( + numpy.ceil(img.array).astype("uint8"), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_names=img.band_names, + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) + + +class Floor(BaseAlgorithm): + """Round data to the largest integer.""" + + title: str = "Round data to the largest integer" + description: str = "Round data to the largest integer." + + # metadata + output_dtype: str = "uint8" + output_min: Sequence[int] = [0] + output_max: Sequence[int] = [255] + + def __call__(self, img: ImageData) -> ImageData: + """Cast Data.""" + return ImageData( + numpy.floor(img.array).astype("uint8"), + assets=img.assets, + crs=img.crs, + bounds=img.bounds, + band_names=img.band_names, + metadata=img.metadata, + cutline_mask=img.cutline_mask, + ) diff --git a/src/titiler/core/titiler/core/dependencies.py b/src/titiler/core/titiler/core/dependencies.py index efc19260c..0b2dbc5c7 100644 --- a/src/titiler/core/titiler/core/dependencies.py +++ b/src/titiler/core/titiler/core/dependencies.py @@ -1,55 +1,65 @@ """Common dependency.""" import json +import warnings from dataclasses import dataclass -from enum import Enum -from typing import Dict, List, Optional, Sequence, Tuple, Union +from typing import Callable, Dict, List, Literal, Optional, Sequence, Tuple, Union import numpy from fastapi import HTTPException, Query +from pydantic import Field from rasterio.crs import CRS -from rasterio.enums import Resampling -from rio_tiler.colormap import cmap, parse_color +from rio_tiler.colormap import ColorMaps +from rio_tiler.colormap import cmap as default_cmap +from rio_tiler.colormap import parse_color from rio_tiler.errors import MissingAssets, MissingBands -from rio_tiler.types import ColorMapType - -ColorMapName = Enum( # type: ignore - "ColorMapName", [(a, a) for a in sorted(cmap.list())] -) -ResamplingName = Enum( # type: ignore - "ResamplingName", [(r.name, r.name) for r in Resampling] -) - - -def ColorMapParams( - colormap_name: ColorMapName = Query(None, description="Colormap name"), - colormap: str = Query(None, description="JSON encoded custom Colormap"), -) -> Optional[ColorMapType]: - """Colormap Dependency.""" - if colormap_name: - return cmap.get(colormap_name.value) - - if colormap: - try: - c = json.loads( - colormap, - object_hook=lambda x: {int(k): parse_color(v) for k, v in x.items()}, - ) +from rio_tiler.types import RIOResampling, WarpResampling +from typing_extensions import Annotated - # Make sure to match colormap type - if isinstance(c, Sequence): - c = [(tuple(inter), parse_color(v)) for (inter, v) in c] - return c - except json.JSONDecodeError as e: - raise HTTPException( - status_code=400, detail="Could not parse the colormap value." - ) from e +def create_colormap_dependency(cmap: ColorMaps) -> Callable: + """Create Colormap Dependency.""" - return None + def deps( + colormap_name: Annotated[ # type: ignore + Literal[tuple(cmap.list())], + Query(description="Colormap name"), + ] = None, + colormap: Annotated[ + Optional[str], Query(description="JSON encoded custom Colormap") + ] = None, + ): + if colormap_name: + return cmap.get(colormap_name) + + if colormap: + try: + c = json.loads( + colormap, + object_hook=lambda x: { + int(k): parse_color(v) for k, v in x.items() + }, + ) + + # Make sure to match colormap type + if isinstance(c, Sequence): + c = [(tuple(inter), parse_color(v)) for (inter, v) in c] + + return c + except json.JSONDecodeError as e: + raise HTTPException( + status_code=400, detail="Could not parse the colormap value." + ) from e + + return None + return deps -def DatasetPathParams(url: str = Query(..., description="Dataset URL")) -> str: + +ColorMapParams = create_colormap_dependency(default_cmap) + + +def DatasetPathParams(url: Annotated[str, Query(description="Dataset URL")]) -> str: """Create dataset path from args""" return url @@ -58,13 +68,12 @@ def DatasetPathParams(url: str = Query(..., description="Dataset URL")) -> str: class DefaultDependency: """Dataclass with dict unpacking""" - def keys(self): - """Return Keys.""" - return self.__dict__.keys() + def as_dict(self, exclude_none: bool = True) -> Dict: + """Transform dataclass to dict.""" + if exclude_none: + return {k: v for k, v in self.__dict__.items() if v is not None} - def __getitem__(self, key): - """Return value.""" - return self.__dict__[key] + return dict(self.__dict__.items()) # Dependencies for simple BaseReader (e.g COGReader) @@ -72,31 +81,40 @@ def __getitem__(self, key): class BidxParams(DefaultDependency): """Band Indexes parameters.""" - indexes: Optional[List[int]] = Query( - None, - title="Band indexes", - alias="bidx", - description="Dataset band indexes", - examples={"one-band": {"value": [1]}, "multi-bands": {"value": [1, 2, 3]}}, - ) + indexes: Annotated[ + Optional[List[int]], + Query( + title="Band indexes", + alias="bidx", + description="Dataset band indexes", + openapi_examples={ + "user-provided": {"value": None}, + "one-band": {"value": [1]}, + "multi-bands": {"value": [1, 2, 3]}, + }, + ), + ] = None @dataclass class ExpressionParams(DefaultDependency): """Expression parameters.""" - expression: Optional[str] = Query( - None, - title="Band Math expression", - description="rio-tiler's band math expression", - examples={ - "simple": {"description": "Simple band math.", "value": "b1/b2"}, - "multi-bands": { - "description": "Semicolon (;) delimited expressions (band1: b1/b2, band2: b2+b3).", - "value": "b1/b2;b2+b3", + expression: Annotated[ + Optional[str], + Query( + title="Band Math expression", + description="rio-tiler's band math expression", + openapi_examples={ + "user-provided": {"value": None}, + "simple": {"description": "Simple band math.", "value": "b1/b2"}, + "multi-bands": { + "description": "Semicolon (;) delimited expressions (band1: b1/b2, band2: b2+b3).", + "value": "b1/b2;b2+b3", + }, }, - }, - ) + ), + ] = None @dataclass @@ -111,76 +129,89 @@ class BidxExprParams(ExpressionParams, BidxParams): class AssetsParams(DefaultDependency): """Assets parameters.""" - assets: List[str] = Query( - None, - title="Asset names", - description="Asset's names.", - examples={ - "one-asset": { - "description": "Return results for asset `data`.", - "value": ["data"], - }, - "multi-assets": { - "description": "Return results for assets `data` and `cog`.", - "value": ["data", "cog"], + assets: Annotated[ + Optional[List[str]], + Query( + title="Asset names", + description="Asset's names.", + openapi_examples={ + "user-provided": {"value": None}, + "one-asset": { + "description": "Return results for asset `data`.", + "value": ["data"], + }, + "multi-assets": { + "description": "Return results for assets `data` and `cog`.", + "value": ["data", "cog"], + }, }, - }, - ) + ), + ] = None + + +def parse_asset_indexes( + asset_indexes: Union[Sequence[str], Dict[str, Sequence[int]]], +) -> Dict[str, Sequence[int]]: + """parse asset indexes parameters.""" + return { + idx.split("|")[0]: list(map(int, idx.split("|")[1].split(","))) + for idx in asset_indexes + } + + +def parse_asset_expression( + asset_expression: Union[Sequence[str], Dict[str, str]], +) -> Dict[str, str]: + """parse asset expression parameters.""" + return {idx.split("|")[0]: idx.split("|")[1] for idx in asset_expression} @dataclass -class AssetsBidxExprParams(DefaultDependency): +class AssetsBidxExprParams(AssetsParams, BidxParams): """Assets, Expression and Asset's band Indexes parameters.""" - assets: Optional[List[str]] = Query( - None, - title="Asset names", - description="Asset's names.", - examples={ - "one-asset": { - "description": "Return results for asset `data`.", - "value": ["data"], - }, - "multi-assets": { - "description": "Return results for assets `data` and `cog`.", - "value": ["data", "cog"], - }, - }, - ) - expression: Optional[str] = Query( - None, - title="Band Math expression", - description="Band math expression between assets", - examples={ - "simple": { - "description": "Return results of expression between assets.", - "value": "asset1_b1 + asset2_b1 / asset3_b1", - }, - }, - ) - - asset_indexes: Optional[Sequence[str]] = Query( - None, - title="Per asset band indexes", - description="Per asset band indexes (coma separated indexes)", - alias="asset_bidx", - examples={ - "one-asset": { - "description": "Return indexes 1,2,3 of asset `data`.", - "value": ["data|1,2,3"], + expression: Annotated[ + Optional[str], + Query( + title="Band Math expression", + description="Band math expression between assets", + openapi_examples={ + "user-provided": {"value": None}, + "simple": { + "description": "Return results of expression between assets.", + "value": "asset1_b1 + asset2_b1 / asset3_b1", + }, }, - "multi-assets": { - "description": "Return indexes 1,2,3 of asset `data` and indexes 1 of asset `cog`", - "value": ["data|1,2,3", "cog|1"], + ), + ] = None + + asset_indexes: Annotated[ + Optional[Sequence[str]], + Query( + title="Per asset band indexes", + description="Per asset band indexes (coma separated indexes)", + alias="asset_bidx", + openapi_examples={ + "user-provided": {"value": None}, + "one-asset": { + "description": "Return indexes 1,2,3 of asset `data`.", + "value": ["data|1,2,3"], + }, + "multi-assets": { + "description": "Return indexes 1,2,3 of asset `data` and indexes 1 of asset `cog`", + "value": ["data|1,2,3", "cog|1"], + }, }, - }, - ) + ), + ] = None - asset_as_band: Optional[bool] = Query( - None, - title="Consider asset as a 1 band dataset", - description="Asset as Band", - ) + asset_as_band: Annotated[ + Optional[bool], + Query( + title="Consider asset as a 1 band dataset", + description="Asset as Band", + ), + ] = None def __post_init__(self): """Post Init.""" @@ -190,10 +221,14 @@ def __post_init__(self): ) if self.asset_indexes: - self.asset_indexes: Dict[str, Sequence[int]] = { # type: ignore - idx.split("|")[0]: list(map(int, idx.split("|")[1].split(","))) - for idx in self.asset_indexes - } + self.asset_indexes = parse_asset_indexes(self.asset_indexes) + + if self.asset_indexes and self.indexes: + warnings.warn( + "Both `asset_bidx` and `bidx` passed; only `asset_bidx` will be considered.", + UserWarning, + stacklevel=1, + ) @dataclass @@ -203,61 +238,73 @@ class AssetsBidxExprParamsOptional(AssetsBidxExprParams): def __post_init__(self): """Post Init.""" if self.asset_indexes: - self.asset_indexes: Dict[str, Sequence[int]] = { # type: ignore - idx.split("|")[0]: list(map(int, idx.split("|")[1].split(","))) - for idx in self.asset_indexes - } + self.asset_indexes = parse_asset_indexes(self.asset_indexes) + + if self.asset_indexes and self.indexes: + warnings.warn( + "Both `asset_bidx` and `bidx` passed; only `asset_bidx` will be considered.", + UserWarning, + stacklevel=1, + ) @dataclass -class AssetsBidxParams(AssetsParams): +class AssetsBidxParams(AssetsParams, BidxParams): """Assets, Asset's band Indexes and Asset's band Expression parameters.""" - asset_indexes: Optional[Sequence[str]] = Query( - None, - title="Per asset band indexes", - description="Per asset band indexes", - alias="asset_bidx", - examples={ - "one-asset": { - "description": "Return indexes 1,2,3 of asset `data`.", - "value": ["data|1;2;3"], - }, - "multi-assets": { - "description": "Return indexes 1,2,3 of asset `data` and indexes 1 of asset `cog`", - "value": ["data|1;2;3", "cog|1"], - }, - }, - ) - - asset_expression: Optional[Sequence[str]] = Query( - None, - title="Per asset band expression", - description="Per asset band expression", - examples={ - "one-asset": { - "description": "Return results for expression `b1*b2+b3` of asset `data`.", - "value": ["data|b1*b2+b3"], + asset_indexes: Annotated[ + Optional[Sequence[str]], + Query( + title="Per asset band indexes", + description="Per asset band indexes", + alias="asset_bidx", + openapi_examples={ + "user-provided": {"value": None}, + "one-asset": { + "description": "Return indexes 1,2,3 of asset `data`.", + "value": ["data|1;2;3"], + }, + "multi-assets": { + "description": "Return indexes 1,2,3 of asset `data` and indexes 1 of asset `cog`", + "value": ["data|1;2;3", "cog|1"], + }, }, - "multi-assets": { - "description": "Return results for expressions `b1*b2+b3` for asset `data` and `b1+b3` for asset `cog`.", - "value": ["data|b1*b2+b3", "cog|b1+b3"], + ), + ] = None + + asset_expression: Annotated[ + Optional[Sequence[str]], + Query( + title="Per asset band expression", + description="Per asset band expression", + openapi_examples={ + "user-provided": {"value": None}, + "one-asset": { + "description": "Return results for expression `b1*b2+b3` of asset `data`.", + "value": ["data|b1*b2+b3"], + }, + "multi-assets": { + "description": "Return results for expressions `b1*b2+b3` for asset `data` and `b1+b3` for asset `cog`.", + "value": ["data|b1*b2+b3", "cog|b1+b3"], + }, }, - }, - ) + ), + ] = None def __post_init__(self): """Post Init.""" if self.asset_indexes: - self.asset_indexes: Dict[str, Sequence[int]] = { # type: ignore - idx.split("|")[0]: list(map(int, idx.split("|")[1].split(","))) - for idx in self.asset_indexes - } + self.asset_indexes = parse_asset_indexes(self.asset_indexes) if self.asset_expression: - self.asset_expression: Dict[str, str] = { # type: ignore - idx.split("|")[0]: idx.split("|")[1] for idx in self.asset_expression - } + self.asset_expression = parse_asset_expression(self.asset_expression) + + if self.asset_indexes and self.indexes: + warnings.warn( + "Both `asset_bidx` and `bidx` passed; only `asset_bidx` will be considered.", + UserWarning, + stacklevel=1, + ) # Dependencies for MultiBandReader @@ -265,21 +312,24 @@ def __post_init__(self): class BandsParams(DefaultDependency): """Band names parameters.""" - bands: List[str] = Query( - None, - title="Band names", - description="Band's names.", - examples={ - "one-band": { - "description": "Return results for band `B01`.", - "value": ["B01"], - }, - "multi-bands": { - "description": "Return results for bands `B01` and `B02`.", - "value": ["B01", "B02"], + bands: Annotated[ + Optional[List[str]], + Query( + title="Band names", + description="Band's names.", + openapi_examples={ + "user-provided": {"value": None}, + "one-band": { + "description": "Return results for band `B01`.", + "value": ["B01"], + }, + "multi-bands": { + "description": "Return results for bands `B01` and `B02`.", + "value": ["B01", "B02"], + }, }, - }, - ) + ), + ] = None @dataclass @@ -302,18 +352,44 @@ def __post_init__(self): @dataclass -class ImageParams(DefaultDependency): - """Common Preview/Crop parameters.""" +class PreviewParams(DefaultDependency): + """Common Preview parameters.""" - max_size: Optional[int] = Query( - 1024, description="Maximum image size to read onto." + # NOTE: sizes dependency can either be a Query or a Path Parameter + max_size: Annotated[int, Field(description="Maximum image size to read onto.")] = ( + 1024 + ) + height: Annotated[ + Optional[int], Field(description="Force output image height.") + ] = None + width: Annotated[Optional[int], Field(description="Force output image width.")] = ( + None ) - height: Optional[int] = Query(None, description="Force output image height.") - width: Optional[int] = Query(None, description="Force output image width.") def __post_init__(self): """Post Init.""" - if self.width and self.height: + if self.width or self.height: + self.max_size = None + + +@dataclass +class PartFeatureParams(DefaultDependency): + """Common parameters for bbox and feature.""" + + # NOTE: the part sizes dependency can either be a Query or a Path Parameter + max_size: Annotated[ + Optional[int], Field(description="Maximum image size to read onto.") + ] = None + height: Annotated[ + Optional[int], Field(description="Force output image height.") + ] = None + width: Annotated[Optional[int], Field(description="Force output image width.")] = ( + None + ) + + def __post_init__(self): + """Post Init.""" + if self.width or self.height: self.max_size = None @@ -321,109 +397,171 @@ def __post_init__(self): class DatasetParams(DefaultDependency): """Low level WarpedVRT Optional parameters.""" - nodata: Optional[Union[str, int, float]] = Query( - None, title="Nodata value", description="Overwrite internal Nodata value" - ) - unscale: Optional[bool] = Query( - False, - title="Apply internal Scale/Offset", - description="Apply internal Scale/Offset", - ) - resampling_method: ResamplingName = Query( - ResamplingName.nearest, # type: ignore - alias="resampling", - description="Resampling method.", - ) + nodata: Annotated[ + Optional[Union[str, int, float]], + Query( + title="Nodata value", + description="Overwrite internal Nodata value", + ), + ] = None + unscale: Annotated[ + Optional[bool], + Query( + title="Apply internal Scale/Offset", + description="Apply internal Scale/Offset. Defaults to `False`.", + ), + ] = None + resampling_method: Annotated[ + Optional[RIOResampling], + Query( + alias="resampling", + description="RasterIO resampling algorithm. Defaults to `nearest`.", + ), + ] = None + reproject_method: Annotated[ + Optional[WarpResampling], + Query( + alias="reproject", + description="WarpKernel resampling algorithm (only used when doing re-projection). Defaults to `nearest`.", + ), + ] = None def __post_init__(self): """Post Init.""" if self.nodata is not None: self.nodata = numpy.nan if self.nodata == "nan" else float(self.nodata) - self.resampling_method = self.resampling_method.value # type: ignore + if self.unscale is not None: + self.unscale = bool(self.unscale) -@dataclass -class ImageRenderingParams(DefaultDependency): - """Image Rendering options.""" - add_mask: bool = Query( - True, alias="return_mask", description="Add mask to the output data." - ) +RescaleType = List[Tuple[float, float]] + +@dataclass +class RenderingParams(DefaultDependency): + """Image Rendering options.""" -RescaleType = List[Tuple[float, ...]] + rescale: Annotated[ + Optional[List[str]], + Query( + title="Min/Max data Rescaling", + description="comma (',') delimited Min,Max range. Can set multiple time for multiple bands.", + examples=["0,2000", "0,1000", "0,10000"], # band 1 # band 2 # band 3 + ), + ] = None + + color_formula: Annotated[ + Optional[str], + Query( + title="Color Formula", + description="rio-color formula (info: https://github.com/mapbox/rio-color)", + ), + ] = None + + def __post_init__(self) -> None: + """Post Init.""" + if self.rescale: + rescale_array = [] + for r in self.rescale: + parsed = tuple( + map( + float, + r.replace(" ", "").replace("[", "").replace("]", "").split(","), + ) + ) + assert ( + len(parsed) == 2 + ), f"Invalid rescale values: {self.rescale}, should be of form ['min,max', 'min,max'] or [[min,max], [min, max]]" + rescale_array.append(parsed) + + self.rescale: RescaleType = rescale_array # type: ignore -def RescalingParams( - rescale: Optional[List[str]] = Query( - None, - title="Min/Max data Rescaling", - description="comma (',') delimited Min,Max range. Can set multiple time for multiple bands.", - example=["0,2000", "0,1000", "0,10000"], # band 1 # band 2 # band 3 - ) -) -> Optional[RescaleType]: - """Min/Max data Rescaling""" - if rescale: - return [tuple(map(float, r.replace(" ", "").split(","))) for r in rescale] +@dataclass +class ImageRenderingParams(RenderingParams): + """Image Rendering options.""" - return None + add_mask: Annotated[ + Optional[bool], + Query( + alias="return_mask", + description="Add mask to the output data. Defaults to `True`", + ), + ] = None @dataclass class StatisticsParams(DefaultDependency): """Statistics options.""" - categorical: bool = Query( - False, description="Return statistics for categorical dataset." - ) - categories: List[Union[float, int]] = Query( - None, - alias="c", - title="Pixels values for categories.", - description="List of values for which to report counts.", - example=[1, 2, 3], - ) - percentiles: List[int] = Query( - [2, 98], - alias="p", - title="Percentile values", - description="List of percentile values.", - example=[2, 5, 95, 98], - ) + categorical: Annotated[ + Optional[bool], + Query( + description="Return statistics for categorical dataset. Defaults to `False`" + ), + ] = None + categories: Annotated[ + Optional[List[Union[float, int]]], + Query( + alias="c", + title="Pixels values for categories.", + description="List of values for which to report counts.", + examples=[1, 2, 3], + ), + ] = None + percentiles: Annotated[ + Optional[List[int]], + Query( + alias="p", + title="Percentile values", + description="List of percentile values (default to [2, 98]).", + examples=[2, 5, 95, 98], + ), + ] = None + + def __post_init__(self): + """Set percentiles default.""" + if not self.percentiles: + self.percentiles = [2, 98] @dataclass class HistogramParams(DefaultDependency): """Numpy Histogram options.""" - bins: Optional[str] = Query( - None, - alias="histogram_bins", - title="Histogram bins.", - description=""" + bins: Annotated[ + Optional[str], + Query( + alias="histogram_bins", + title="Histogram bins.", + description=""" Defines the number of equal-width bins in the given range (10, by default). If bins is a sequence (comma `,` delimited values), it defines a monotonically increasing array of bin edges, including the rightmost edge, allowing for non-uniform bin widths. link: https://numpy.org/doc/stable/reference/generated/numpy.histogram.html - """, - examples={ - "simple": { - "description": "Defines the number of equal-width bins", - "value": 8, - }, - "array": { - "description": "Defines custom bin edges (comma `,` delimited values)", - "value": "0,100,200,300", + """, + openapi_examples={ + "user-provided": {"value": None}, + "simple": { + "description": "Defines the number of equal-width bins", + "value": 8, + }, + "array": { + "description": "Defines custom bin edges (comma `,` delimited values)", + "value": "0,100,200,300", + }, }, - }, - ) - - range: Optional[str] = Query( - None, - alias="histogram_range", - title="Histogram range", - description=""" + ), + ] = None + + range: Annotated[ + Optional[str], + Query( + alias="histogram_range", + title="Histogram range", + description=""" Comma `,` delimited range of the bins. The lower and upper range of the bins. If not provided, range is simply (a.min(), a.max()). @@ -432,9 +570,10 @@ class HistogramParams(DefaultDependency): range affects the automatic bin computation as well. link: https://numpy.org/doc/stable/reference/generated/numpy.histogram.html - """, - example="0,1000", - ) + """, + examples="0,1000", + ), + ] = None def __post_init__(self): """Post Init.""" @@ -448,15 +587,22 @@ def __post_init__(self): self.bins = 10 if self.range: - self.range = list(map(float, self.range.split(","))) # type: ignore + parsed = list(map(float, self.range.split(","))) + assert ( + len(parsed) == 2 + ), f"Invalid histogram_range values: {self.range}, should be of form 'min,max'" + + self.range = parsed # type: ignore def CoordCRSParams( - crs: str = Query( - None, - alias="coord-crs", - description="Coordinate Reference System of the input coords. Default to `epsg:4326`.", - ) + crs: Annotated[ + Optional[str], + Query( + alias="coord_crs", + description="Coordinate Reference System of the input coords. Default to `epsg:4326`.", + ), + ] = None, ) -> Optional[CRS]: """Coordinate Reference System Coordinates Param.""" if crs: @@ -466,14 +612,68 @@ def CoordCRSParams( def DstCRSParams( - crs: str = Query( - None, - alias="dst-crs", - description="Output Coordinate Reference System.", - ) + crs: Annotated[ + Optional[str], + Query( + alias="dst_crs", + description="Output Coordinate Reference System.", + ), + ] = None, ) -> Optional[CRS]: """Coordinate Reference System Coordinates Param.""" if crs: return CRS.from_user_input(crs) return None + + +def CRSParams( + crs: Annotated[ + Optional[str], + Query( + description="Coordinate Reference System.", + ), + ] = None, +) -> Optional[CRS]: + """Coordinate Reference System Coordinates Param.""" + if crs: + return CRS.from_user_input(crs) + + return None + + +def BufferParams( + buffer: Annotated[ + Optional[float], + Query( + gt=0, + title="Tile buffer.", + description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", + ), + ] = None, +) -> Optional[float]: + """Tile buffer Parameter.""" + return buffer + + +@dataclass +class TileParams(DefaultDependency): + """Tile options.""" + + buffer: Annotated[ + Optional[float], + Query( + gt=0, + title="Tile buffer.", + description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", + ), + ] = None + + padding: Annotated[ + Optional[int], + Query( + gt=0, + title="Tile padding.", + description="Padding to apply to each tile edge. Helps reduce resampling artefacts along edges. Defaults to `0`.", + ), + ] = None diff --git a/src/titiler/core/titiler/core/errors.py b/src/titiler/core/titiler/core/errors.py index 141aadcc7..3d517f284 100644 --- a/src/titiler/core/titiler/core/errors.py +++ b/src/titiler/core/titiler/core/errors.py @@ -15,7 +15,7 @@ ) from starlette import status from starlette.requests import Request -from starlette.responses import JSONResponse +from starlette.responses import JSONResponse, Response class TilerError(Exception): @@ -52,6 +52,9 @@ def exception_handler_factory(status_code: int) -> Callable: """ def handler(request: Request, exc: Exception): + if status_code == status.HTTP_204_NO_CONTENT: + return Response(content=None, status_code=204) + return JSONResponse(content={"detail": str(exc)}, status_code=status_code) return handler @@ -63,5 +66,5 @@ def add_exception_handlers( """ Add exception handlers to the FastAPI app. """ - for (exc, code) in status_codes.items(): + for exc, code in status_codes.items(): app.add_exception_handler(exc, exception_handler_factory(code)) diff --git a/src/titiler/core/titiler/core/factory.py b/src/titiler/core/titiler/core/factory.py index 52e16a3d6..6ff8be4ff 100644 --- a/src/titiler/core/titiler/core/factory.py +++ b/src/titiler/core/titiler/core/factory.py @@ -1,30 +1,50 @@ """TiTiler Router factories.""" import abc -from dataclasses import dataclass, field -from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Type, Union +import logging +import warnings +from typing import ( + Any, + Callable, + Dict, + List, + Literal, + Optional, + Sequence, + Set, + Tuple, + Type, + Union, +) from urllib.parse import urlencode import jinja2 +import numpy import rasterio +from attrs import define, field from fastapi import APIRouter, Body, Depends, Path, Query from fastapi.dependencies.utils import get_parameterless_sub_dependant from fastapi.params import Depends as DependsFunc from geojson_pydantic.features import Feature, FeatureCollection -from geojson_pydantic.geometries import Polygon from morecantile import TileMatrixSet from morecantile import tms as morecantile_tms from morecantile.defaults import TileMatrixSets -from rasterio.crs import CRS +from morecantile.models import crs_axis_inverted +from pydantic import Field +from rio_tiler.colormap import ColorMaps +from rio_tiler.colormap import cmap as default_cmap from rio_tiler.constants import WGS84_CRS from rio_tiler.io import BaseReader, MultiBandReader, MultiBaseReader, Reader -from rio_tiler.models import BandStatistics, Bounds, Info +from rio_tiler.models import Bounds, ImageData, Info from rio_tiler.types import ColorMapType -from rio_tiler.utils import get_array_statistics +from rio_tiler.utils import CRS_to_uri, CRS_to_urn from starlette.requests import Request from starlette.responses import HTMLResponse, Response -from starlette.routing import Match, compile_path, replace_params +from starlette.routing import Match, NoMatchFound +from starlette.routing import Route as APIRoute +from starlette.routing import compile_path, replace_params from starlette.templating import Jinja2Templates +from typing_extensions import Annotated from titiler.core.algorithm import AlgorithmMetadata, Algorithms, BaseAlgorithm from titiler.core.algorithm import algorithms as available_algorithms @@ -39,20 +59,22 @@ BidxExprParams, ColorMapParams, CoordCRSParams, + CRSParams, DatasetParams, DatasetPathParams, DefaultDependency, DstCRSParams, HistogramParams, - ImageParams, ImageRenderingParams, - RescaleType, - RescalingParams, + PartFeatureParams, + PreviewParams, StatisticsParams, + TileParams, ) from titiler.core.models.mapbox import TileJSON -from titiler.core.models.OGC import TileMatrixSetList +from titiler.core.models.OGC import TileMatrixSetList, TileSet, TileSetList from titiler.core.models.responses import ( + ColorMapsList, InfoGeoJSON, MultiBaseInfo, MultiBaseInfoGeoJSON, @@ -62,15 +84,16 @@ Statistics, StatisticsGeoJSON, ) -from titiler.core.resources.enums import ImageType, MediaType, OptionalHeader +from titiler.core.resources.enums import ImageType from titiler.core.resources.responses import GeoJSONResponse, JSONResponse, XMLResponse from titiler.core.routing import EndpointScope +from titiler.core.telemetry import factory_trace +from titiler.core.utils import bounds_to_geometry, render_image -DEFAULT_TEMPLATES = Jinja2Templates( - directory="", - loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]), -) # type:ignore - +jinja2_env = jinja2.Environment( + loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]) +) +DEFAULT_TEMPLATES = Jinja2Templates(env=jinja2_env) img_endpoint_params: Dict[str, Any] = { "responses": { @@ -90,95 +113,60 @@ "response_class": Response, } +logger = logging.getLogger(__name__) + -@dataclass # type: ignore +@define class FactoryExtension(metaclass=abc.ABCMeta): """Factory Extension.""" @abc.abstractmethod - def register(self, factory: "BaseTilerFactory"): + def register(self, factory: "BaseFactory"): """Register extension to the factory.""" ... -# ref: https://github.com/python/mypy/issues/5374 -@dataclass # type: ignore -class BaseTilerFactory(metaclass=abc.ABCMeta): - """BaseTiler Factory. +@define(kw_only=True) +class BaseFactory(metaclass=abc.ABCMeta): + """Base Factory. Abstract Base Class which defines most inputs used by dynamic tiler. Attributes: - reader (rio_tiler.io.base.BaseReader): A rio-tiler reader (e.g Reader). router (fastapi.APIRouter): Application router to register endpoints to. - path_dependency (Callable): Endpoint dependency defining `path` to pass to the reader init. - dataset_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining dataset overwriting options (e.g nodata). - layer_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining dataset indexes/bands/assets options. - render_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining image rendering options (e.g add_mask). - colormap_dependency (Callable): Endpoint dependency defining ColorMap options (e.g colormap_name). - process_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining image post-processing options (e.g rescaling, color-formula). - tms_dependency (Callable): Endpoint dependency defining TileMatrixSet to use. - reader_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining BaseReader options. - environment_dependency (Callable): Endpoint dependency to define GDAL environment at runtime. router_prefix (str): prefix where the router will be mounted in the application. - optional_headers(sequence of titiler.core.resources.enums.OptionalHeader): additional headers to return with the response. + route_dependencies (list): Additional routes dependencies to add after routes creations. """ - reader: Type[BaseReader] - # FastAPI router - router: APIRouter = field(default_factory=APIRouter) - - # Path Dependency - path_dependency: Callable[..., Any] = DatasetPathParams - - # Rasterio Dataset Options (nodata, unscale, resampling) - dataset_dependency: Type[DefaultDependency] = DatasetParams - - # Indexes/Expression Dependencies - layer_dependency: Type[DefaultDependency] = BidxExprParams - - # Image rendering Dependencies - render_dependency: Type[DefaultDependency] = ImageRenderingParams - colormap_dependency: Callable[..., Optional[ColorMapType]] = ColorMapParams - - rescale_dependency: Callable[..., Optional[RescaleType]] = RescalingParams - - # Post Processing Dependencies (algorithm) - process_dependency: Callable[ - ..., Optional[BaseAlgorithm] - ] = available_algorithms.dependency - - # Reader dependency - reader_dependency: Type[DefaultDependency] = DefaultDependency - - # GDAL ENV dependency - environment_dependency: Callable[..., Dict] = field(default=lambda: {}) - - # TileMatrixSet dependency - supported_tms: TileMatrixSets = morecantile_tms - default_tms: str = "WebMercatorQuad" + router: APIRouter = field(factory=APIRouter) # Router Prefix is needed to find the path for /tile if the TilerFactory.router is mounted # with other router (multiple `.../tile` routes). # e.g if you mount the route with `/cog` prefix, set router_prefix to cog and router_prefix: str = "" - # add additional headers in response - optional_headers: List[OptionalHeader] = field(default_factory=list) - # add dependencies to specific routes route_dependencies: List[Tuple[List[EndpointScope], List[DependsFunc]]] = field( - default_factory=list + factory=list ) - extensions: List[FactoryExtension] = field(default_factory=list) + extensions: List[FactoryExtension] = field(factory=list) - templates: Jinja2Templates = DEFAULT_TEMPLATES + name: Optional[str] = field(default=None) + operation_prefix: str = field(init=False, default="") + + conforms_to: Set[str] = field(factory=set) + + enable_telemetry: bool = field(default=False) - def __post_init__(self): + def __attrs_post_init__(self): """Post Init: register route and configure specific options.""" + # prefix for endpoint's operationId + name = self.name or self.router_prefix.replace("/", ".") + self.operation_prefix = f"{name}." if name else "" + # Register endpoints self.register_routes() @@ -190,9 +178,12 @@ def __post_init__(self): for scopes, dependencies in self.route_dependencies: self.add_route_dependencies(scopes=scopes, dependencies=dependencies) + if self.enable_telemetry: + self.add_telemetry() + @abc.abstractmethod def register_routes(self): - """Register Tiler Routes.""" + """Register Routes.""" ... def url_for(self, request: Request, name: str, **path_params: Any) -> str: @@ -206,7 +197,7 @@ def url_for(self, request: Request, name: str, **path_params: Any) -> str: if "{" in prefix: _, path_format, param_convertors = compile_path(prefix) prefix, _ = replace_params( - path_format, param_convertors, request.path_params + path_format, param_convertors, request.path_params.copy() ) base_url += prefix @@ -235,7 +226,8 @@ def add_route_dependencies( route.dependant.dependencies.insert( # type: ignore 0, get_parameterless_sub_dependant( - depends=depends, path=route.path_format # type: ignore + depends=depends, + path=route.path_format, # type: ignore ), ) @@ -245,37 +237,119 @@ def add_route_dependencies( # https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/routing.py#L677-L678 route.dependencies.extend(dependencies) # type: ignore + def add_telemetry(self): + """ + Applies the factory_trace decorator to all registered API routes. + + This method iterates through the router's routes and wraps the endpoint + of each APIRoute to ensure consistent OpenTelemetry tracing. + """ + if not factory_trace.decorator_enabled: + warnings.warn( + "telemetry enabled for the factory class but tracing is not available", + RuntimeWarning, + stacklevel=2, + ) + return + + for route in self.router.routes: + if isinstance(route, APIRoute): + route.endpoint = factory_trace(route.endpoint, factory_instance=self) + -@dataclass -class TilerFactory(BaseTilerFactory): +@define(kw_only=True) +class TilerFactory(BaseFactory): """Tiler Factory. Attributes: reader (rio_tiler.io.base.BaseReader): A rio-tiler reader. Defaults to `rio_tiler.io.Reader`. + reader_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining BaseReader options. + path_dependency (Callable): Endpoint dependency defining `path` to pass to the reader init. + layer_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining dataset indexes/bands/assets options. + dataset_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining dataset overwriting options (e.g nodata). + tile_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining tile options (e.g buffer, padding). stats_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining options for rio-tiler's statistics method. histogram_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining options for numpy's histogram method. - img_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining options for rio-tiler's preview/crop method. + img_preview_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining options for rio-tiler's preview method. + img_part_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining options for rio-tiler's part/feature methods. + process_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining image post-processing options (e.g rescaling, color-formula). + rescale_dependency (Callable[..., Optional[RescaleType]]): + color_formula_dependency (Callable[..., Optional[str]]): + colormap_dependency (Callable): Endpoint dependency defining ColorMap options (e.g colormap_name). + render_dependency (titiler.core.dependencies.DefaultDependency): Endpoint dependency defining image rendering options (e.g add_mask). + environment_dependency (Callable): Endpoint dependency to define GDAL environment at runtime. + supported_tms (morecantile.defaults.TileMatrixSets): TileMatrixSets object holding the supported TileMatrixSets. + templates (Jinja2Templates): Jinja2 templates. add_preview (bool): add `/preview` endpoints. Defaults to True. - add_part (bool): add `/crop` endpoints. Defaults to True. - add_viewer (bool): add `/map` endpoints. Defaults to True. + add_part (bool): add `/bbox` and `/feature` endpoints. Defaults to True. + add_viewer (bool): add `/map.html` endpoints. Defaults to True. """ # Default reader is set to rio_tiler.io.Reader reader: Type[BaseReader] = Reader + # Reader dependency + reader_dependency: Type[DefaultDependency] = DefaultDependency + + # Path Dependency + path_dependency: Callable[..., Any] = DatasetPathParams + + # Indexes/Expression Dependencies + layer_dependency: Type[DefaultDependency] = BidxExprParams + + # Rasterio Dataset Options (nodata, unscale, resampling, reproject) + dataset_dependency: Type[DefaultDependency] = DatasetParams + + # Tile/Tilejson/WMTS Dependencies + tile_dependency: Type[DefaultDependency] = TileParams + # Statistics/Histogram Dependencies stats_dependency: Type[DefaultDependency] = StatisticsParams histogram_dependency: Type[DefaultDependency] = HistogramParams # Crop/Preview endpoints Dependencies - img_dependency: Type[DefaultDependency] = ImageParams + img_preview_dependency: Type[DefaultDependency] = PreviewParams + img_part_dependency: Type[DefaultDependency] = PartFeatureParams + + # Post Processing Dependencies (algorithm) + process_dependency: Callable[..., Optional[BaseAlgorithm]] = ( + available_algorithms.dependency + ) + + # Image rendering Dependencies + colormap_dependency: Callable[..., Optional[ColorMapType]] = ColorMapParams + render_dependency: Type[DefaultDependency] = ImageRenderingParams + + # GDAL ENV dependency + environment_dependency: Callable[..., Dict] = field(default=lambda: {}) + + # TileMatrixSet dependency + supported_tms: TileMatrixSets = morecantile_tms + + templates: Jinja2Templates = DEFAULT_TEMPLATES + + render_func: Callable[..., Tuple[bytes, str]] = render_image # Add/Remove some endpoints add_preview: bool = True add_part: bool = True add_viewer: bool = True + conforms_to: Set[str] = field( + factory=lambda: { + # https://docs.ogc.org/is/20-057/20-057.html#toc30 + "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/tileset", + # https://docs.ogc.org/is/20-057/20-057.html#toc34 + "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/tilesets-list", + # https://docs.ogc.org/is/20-057/20-057.html#toc65 + "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/core", + "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/png", + "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/jpeg", + "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/tiff", + } + ) + def register_routes(self): """ This Method register routes to the router. @@ -290,9 +364,12 @@ def register_routes(self): self.bounds() self.info() self.statistics() + self.tilesets() self.tile() - self.tilejson() + if self.add_viewer: + self.map_viewer() self.wmts() + self.tilejson() self.point() # Optional Routes @@ -302,9 +379,6 @@ def register_routes(self): if self.add_part: self.part() - if self.add_viewer: - self.map_viewer() - ############################################################################ # /bounds ############################################################################ @@ -315,16 +389,22 @@ def bounds(self): "/bounds", response_model=Bounds, responses={200: {"description": "Return dataset's bounds."}}, + operation_id=f"{self.operation_prefix}getBounds", ) def bounds( src_path=Depends(self.path_dependency), reader_params=Depends(self.reader_dependency), + crs=Depends(CRSParams), env=Depends(self.environment_dependency), ): """Return the bounds of the COG.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - return {"bounds": src_dst.geographic_bounds} + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + crs = crs or WGS84_CRS + return { + "bounds": src_dst.get_geographic_bounds(crs or WGS84_CRS), + "crs": CRS_to_uri(crs) or crs.to_wkt(), + } ############################################################################ # /info @@ -338,6 +418,7 @@ def info(self): response_model_exclude_none=True, response_class=JSONResponse, responses={200: {"description": "Return dataset's basic info."}}, + operation_id=f"{self.operation_prefix}getInfo", ) def info( src_path=Depends(self.path_dependency), @@ -346,7 +427,7 @@ def info( ): """Return dataset's basic info.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + with self.reader(src_path, **reader_params.as_dict()) as src_dst: return src_dst.info() @self.router.get( @@ -360,18 +441,24 @@ def info( "description": "Return dataset's basic info as a GeoJSON feature.", } }, + operation_id=f"{self.operation_prefix}getInfoGeoJSON", ) def info_geojson( src_path=Depends(self.path_dependency), reader_params=Depends(self.reader_dependency), + crs=Depends(CRSParams), env=Depends(self.environment_dependency), ): """Return dataset's basic info as a GeoJSON feature.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS) + geometry = bounds_to_geometry(bounds) + return Feature( type="Feature", - geometry=Polygon.from_bounds(*src_dst.geographic_bounds), + bbox=bounds, + geometry=geometry, properties=src_dst.info(), ) @@ -392,26 +479,34 @@ def statistics(self): "description": "Return dataset's statistics.", } }, + operation_id=f"{self.operation_prefix}getStatistics", ) def statistics( src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), + image_params=Depends(self.img_preview_dependency), + post_process=Depends(self.process_dependency), stats_params=Depends(self.stats_dependency), histogram_params=Depends(self.histogram_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Get Dataset statistics.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - return src_dst.statistics( - **layer_params, - **image_params, - **dataset_params, - **stats_params, - hist_options={**histogram_params}, + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + image = src_dst.preview( + **layer_params.as_dict(), + **image_params.as_dict(), + **dataset_params.as_dict(), + ) + + if post_process: + image = post_process(image) + + return image.statistics( + **stats_params.as_dict(), + hist_options=histogram_params.as_dict(), ) # POST endpoint @@ -422,23 +517,27 @@ def statistics( response_class=GeoJSONResponse, responses={ 200: { - "content": {"application/json": {}}, - "description": "Return dataset's statistics.", + "content": {"application/geo+json": {}}, + "description": "Return dataset's statistics from feature or featureCollection.", } }, + operation_id=f"{self.operation_prefix}postStatisticsForGeoJSON", ) def geojson_statistics( - geojson: Union[FeatureCollection, Feature] = Body( - ..., description="GeoJSON Feature or FeatureCollection." - ), + geojson: Annotated[ + Union[FeatureCollection, Feature], + Body(description="GeoJSON Feature or FeatureCollection."), + ], src_path=Depends(self.path_dependency), - coord_crs: Optional[CRS] = Depends(CoordCRSParams), + reader_params=Depends(self.reader_dependency), + coord_crs=Depends(CoordCRSParams), + dst_crs=Depends(DstCRSParams), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), + image_params=Depends(self.img_part_dependency), + post_process=Depends(self.process_dependency), stats_params=Depends(self.stats_dependency), histogram_params=Depends(self.histogram_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Get Statistics from a geojson feature or featureCollection.""" @@ -447,34 +546,266 @@ def geojson_statistics( fc = FeatureCollection(type="FeatureCollection", features=[geojson]) with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - for feature in fc: - data = src_dst.feature( - feature.dict(exclude_none=True), + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + for feature in fc.features: + shape = feature.model_dump(exclude_none=True) + image = src_dst.feature( + shape, + shape_crs=coord_crs or WGS84_CRS, + dst_crs=dst_crs, + align_bounds_with_dataset=True, + **layer_params.as_dict(), + **image_params.as_dict(), + **dataset_params.as_dict(), + ) + + # Get the coverage % array + coverage_array = image.get_coverage_array( + shape, shape_crs=coord_crs or WGS84_CRS, - **layer_params, - **image_params, - **dataset_params, ) - stats = get_array_statistics( - data.as_masked(), - **stats_params, - **histogram_params, + + if post_process: + image = post_process(image) + + stats = image.statistics( + **stats_params.as_dict(), + hist_options=histogram_params.as_dict(), + coverage=coverage_array, ) feature.properties = feature.properties or {} - feature.properties.update( + feature.properties.update({"statistics": stats}) + + return fc.features[0] if isinstance(geojson, Feature) else fc + + ############################################################################ + # /tileset + ############################################################################ + def tilesets(self): + """Register OGC tilesets endpoints.""" + + @self.router.get( + "/tiles", + response_model=TileSetList, + response_class=JSONResponse, + response_model_exclude_none=True, + responses={ + 200: { + "content": { + "application/json": {}, + } + } + }, + summary="Retrieve a list of available raster tilesets for the specified dataset.", + operation_id=f"{self.operation_prefix}getTileSetList", + ) + async def tileset_list( + request: Request, + src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), + crs=Depends(CRSParams), + env=Depends(self.environment_dependency), + ): + """Retrieve a list of available raster tilesets for the specified dataset.""" + with rasterio.Env(**env): + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS) + + collection_bbox = { + "lowerLeft": [bounds[0], bounds[1]], + "upperRight": [bounds[2], bounds[3]], + "crs": CRS_to_uri(crs or WGS84_CRS), + } + + qs = [ + (key, value) + for (key, value) in request.query_params._list + if key.lower() not in ["crs"] + ] + query_string = f"?{urlencode(qs)}" if qs else "" + + tilesets = [] + for tms in self.supported_tms.list(): + tileset = { + "title": f"tileset tiled using {tms} TileMatrixSet", + "dataType": "map", + "crs": self.supported_tms.get(tms).crs, + "boundingBox": collection_bbox, + "links": [ + { + "href": self.url_for( + request, "tileset", tileMatrixSetId=tms + ) + + query_string, + "rel": "self", + "type": "application/json", + "title": f"Tileset tiled using {tms} TileMatrixSet", + }, + { + "href": self.url_for( + request, + "tile", + tileMatrixSetId=tms, + z="{z}", + x="{x}", + y="{y}", + ) + + query_string, + "rel": "tile", + "title": "Templated link for retrieving Raster tiles", + }, + ], + } + + try: + tileset["links"].append( + { + "href": str( + request.url_for("tilematrixset", tileMatrixSetId=tms) + ), + "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", + "type": "application/json", + "title": f"Definition of '{tms}' tileMatrixSet", + } + ) + except NoMatchFound: + pass + + tilesets.append(tileset) + + data = TileSetList.model_validate({"tilesets": tilesets}) + return data + + @self.router.get( + "/tiles/{tileMatrixSetId}", + response_model=TileSet, + response_class=JSONResponse, + response_model_exclude_none=True, + responses={200: {"content": {"application/json": {}}}}, + summary="Retrieve the raster tileset metadata for the specified dataset and tiling scheme (tile matrix set).", + operation_id=f"{self.operation_prefix}getTileSet", + ) + async def tileset( + request: Request, + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], + src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), + env=Depends(self.environment_dependency), + ): + """Retrieve the raster tileset metadata for the specified dataset and tiling scheme (tile matrix set).""" + tms = self.supported_tms.get(tileMatrixSetId) + with rasterio.Env(**env): + with self.reader( + src_path, tms=tms, **reader_params.as_dict() + ) as src_dst: + bounds = src_dst.get_geographic_bounds(tms.rasterio_geographic_crs) + minzoom = src_dst.minzoom + maxzoom = src_dst.maxzoom + + collection_bbox = { + "lowerLeft": [bounds[0], bounds[1]], + "upperRight": [bounds[2], bounds[3]], + "crs": CRS_to_uri(tms.rasterio_geographic_crs), + } + + tilematrix_limit = [] + for zoom in range(minzoom, maxzoom + 1, 1): + matrix = tms.matrix(zoom) + ulTile = tms.tile(bounds[0], bounds[3], int(matrix.id)) + lrTile = tms.tile(bounds[2], bounds[1], int(matrix.id)) + minx, maxx = (min(ulTile.x, lrTile.x), max(ulTile.x, lrTile.x)) + miny, maxy = (min(ulTile.y, lrTile.y), max(ulTile.y, lrTile.y)) + tilematrix_limit.append( { - "statistics": { - f"{data.band_names[ix]}": BandStatistics( - **stats[ix] - ) - for ix in range(len(stats)) - } + "tileMatrix": matrix.id, + "minTileRow": max(miny, 0), + "maxTileRow": min(maxy, matrix.matrixHeight), + "minTileCol": max(minx, 0), + "maxTileCol": min(maxx, matrix.matrixWidth), } ) - return fc.features[0] if isinstance(geojson, Feature) else fc + query_string = ( + f"?{urlencode(request.query_params._list)}" + if request.query_params._list + else "" + ) + + links = [ + { + "href": self.url_for( + request, + "tileset", + tileMatrixSetId=tileMatrixSetId, + ), + "rel": "self", + "type": "application/json", + "title": f"Tileset tiled using {tileMatrixSetId} TileMatrixSet", + }, + { + "href": self.url_for( + request, + "tile", + tileMatrixSetId=tileMatrixSetId, + z="{z}", + x="{x}", + y="{y}", + ) + + query_string, + "rel": "tile", + "title": "Templated link for retrieving Raster tiles", + "templated": True, + }, + ] + try: + links.append( + { + "href": str( + request.url_for( + "tilematrixset", tileMatrixSetId=tileMatrixSetId + ) + ), + "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", + "type": "application/json", + "title": f"Definition of '{tileMatrixSetId}' tileMatrixSet", + } + ) + except NoMatchFound: + pass + + if self.add_viewer: + links.append( + { + "href": self.url_for( + request, + "map_viewer", + tileMatrixSetId=tileMatrixSetId, + ) + + query_string, + "type": "text/html", + "rel": "data", + "title": f"Map viewer for '{tileMatrixSetId}' tileMatrixSet", + } + ) + + data = TileSet.model_validate( + { + "title": f"tileset tiled using {tileMatrixSetId} TileMatrixSet", + "dataType": "map", + "crs": tms.crs, + "boundingBox": collection_bbox, + "links": links, + "tileMatrixSetLimits": tilematrix_limit, + } + ) + + return data ############################################################################ # /tiles @@ -482,146 +813,149 @@ def geojson_statistics( def tile(self): # noqa: C901 """Register /tiles endpoint.""" - @self.router.get(r"/tiles/{z}/{x}/{y}", **img_endpoint_params) - @self.router.get(r"/tiles/{z}/{x}/{y}.{format}", **img_endpoint_params) - @self.router.get(r"/tiles/{z}/{x}/{y}@{scale}x", **img_endpoint_params) - @self.router.get(r"/tiles/{z}/{x}/{y}@{scale}x.{format}", **img_endpoint_params) - @self.router.get(r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}", **img_endpoint_params) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}.{format}", **img_endpoint_params + "/tiles/{tileMatrixSetId}/{z}/{x}/{y}", + operation_id=f"{self.operation_prefix}getTile", + **img_endpoint_params, + ) + @self.router.get( + "/tiles/{tileMatrixSetId}/{z}/{x}/{y}.{format}", + operation_id=f"{self.operation_prefix}getTileWithFormat", + **img_endpoint_params, ) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}@{scale}x", **img_endpoint_params + "/tiles/{tileMatrixSetId}/{z}/{x}/{y}@{scale}x", + operation_id=f"{self.operation_prefix}getTileWithScale", + **img_endpoint_params, ) @self.router.get( - r"/tiles/{TileMatrixSetId}/{z}/{x}/{y}@{scale}x.{format}", + "/tiles/{tileMatrixSetId}/{z}/{x}/{y}@{scale}x.{format}", + operation_id=f"{self.operation_prefix}getTileWithFormatAndScale", **img_endpoint_params, ) def tile( - z: int = Path(..., ge=0, le=30, description="TMS tiles's zoom level"), - x: int = Path(..., description="TMS tiles's column"), - y: int = Path(..., description="TMS tiles's row"), - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), - scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - format: ImageType = Query( - None, description="Output image type. Default is auto." - ), + z: Annotated[ + int, + Path( + description="Identifier (Z) selecting one of the scales defined in the TileMatrixSet and representing the scaleDenominator the tile.", + ), + ], + x: Annotated[ + int, + Path( + description="Column (X) index of the tile on the selected TileMatrix. It cannot exceed the MatrixHeight-1 for the selected TileMatrix.", + ), + ], + y: Annotated[ + int, + Path( + description="Row (Y) index of the tile on the selected TileMatrix. It cannot exceed the MatrixWidth-1 for the selected TileMatrix.", + ), + ], + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], + scale: Annotated[ + int, + Field( + gt=0, le=4, description="Tile size scale. 1=256x256, 2=512x512..." + ), + ] = 1, + format: Annotated[ + ImageType, + Field( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg)." + ), + ] = None, src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), + tile_params=Depends(self.tile_dependency), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), - buffer: Optional[float] = Query( - None, - gt=0, - title="Tile buffer.", - description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", - ), post_process=Depends(self.process_dependency), - rescale=Depends(self.rescale_dependency), - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), colormap=Depends(self.colormap_dependency), render_params=Depends(self.render_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Create map tile from a dataset.""" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): - with self.reader(src_path, tms=tms, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader( + src_path, tms=tms, **reader_params.as_dict() + ) as src_dst: image = src_dst.tile( x, y, z, tilesize=scale * 256, - buffer=buffer, - **layer_params, - **dataset_params, + **tile_params.as_dict(), + **layer_params.as_dict(), + **dataset_params.as_dict(), ) dst_colormap = getattr(src_dst, "colormap", None) if post_process: image = post_process(image) - if rescale: - image.rescale(rescale) - - if color_formula: - image.apply_color_formula(color_formula) - - if cmap := colormap or dst_colormap: - image = image.apply_colormap(cmap) - - if not format: - format = ImageType.jpeg if image.mask.all() else ImageType.png - - content = image.render( - img_format=format.driver, - **format.profile, - **render_params, + content, media_type = self.render_func( + image, + output_format=format, + colormap=colormap or dst_colormap, + **render_params.as_dict(), ) - return Response(content, media_type=format.mediatype) + return Response(content, media_type=media_type) def tilejson(self): # noqa: C901 """Register /tilejson.json endpoint.""" @self.router.get( - "/tilejson.json", - response_model=TileJSON, - responses={200: {"description": "Return a tilejson"}}, - response_model_exclude_none=True, - ) - @self.router.get( - "/{TileMatrixSetId}/tilejson.json", + "/{tileMatrixSetId}/tilejson.json", response_model=TileJSON, responses={200: {"description": "Return a tilejson"}}, response_model_exclude_none=True, + operation_id=f"{self.operation_prefix}getTileJSON", ) def tilejson( request: Request, - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], + tile_format: Annotated[ + Optional[ImageType], + Query( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", + ), + ] = None, + tile_scale: Annotated[ + int, + Query( + gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." + ), + ] = 1, + minzoom: Annotated[ + Optional[int], + Query(description="Overwrite default minzoom."), + ] = None, + maxzoom: Annotated[ + Optional[int], + Query(description="Overwrite default maxzoom."), + ] = None, src_path=Depends(self.path_dependency), - tile_format: Optional[ImageType] = Query( - None, description="Output image type. Default is auto." - ), - tile_scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - minzoom: Optional[int] = Query( - None, description="Overwrite default minzoom." - ), - maxzoom: Optional[int] = Query( - None, description="Overwrite default maxzoom." - ), - layer_params=Depends(self.layer_dependency), # noqa - dataset_params=Depends(self.dataset_dependency), # noqa - buffer: Optional[float] = Query( # noqa - None, - gt=0, - title="Tile buffer.", - description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", - ), - post_process=Depends(self.process_dependency), # noqa - rescale=Depends(self.rescale_dependency), # noqa - color_formula: Optional[str] = Query( # noqa - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), - colormap=Depends(self.colormap_dependency), # noqa - render_params=Depends(self.render_dependency), # noqa reader_params=Depends(self.reader_dependency), + tile_params=Depends(self.tile_dependency), + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), + post_process=Depends(self.process_dependency), + colormap=Depends(self.colormap_dependency), + render_params=Depends(self.render_dependency), env=Depends(self.environment_dependency), ): """Return TileJSON document for a dataset.""" @@ -630,7 +964,7 @@ def tilejson( "x": "{x}", "y": "{y}", "scale": tile_scale, - "TileMatrixSetId": TileMatrixSetId, + "tileMatrixSetId": tileMatrixSetId, } if tile_format: route_params["format"] = tile_format.value @@ -651,75 +985,86 @@ def tilejson( if qs: tiles_url += f"?{urlencode(qs)}" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): - with self.reader(src_path, tms=tms, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader( + src_path, tms=tms, **reader_params.as_dict() + ) as src_dst: return { - "bounds": src_dst.geographic_bounds, + "bounds": src_dst.get_geographic_bounds( + tms.rasterio_geographic_crs + ), "minzoom": minzoom if minzoom is not None else src_dst.minzoom, "maxzoom": maxzoom if maxzoom is not None else src_dst.maxzoom, "tiles": [tiles_url], } def map_viewer(self): # noqa: C901 - """Register /map endpoint.""" + """Register /map.html endpoint.""" - @self.router.get("/map", response_class=HTMLResponse) - @self.router.get("/{TileMatrixSetId}/map", response_class=HTMLResponse) + @self.router.get( + "/{tileMatrixSetId}/map.html", + response_class=HTMLResponse, + operation_id=f"{self.operation_prefix}getMapViewer", + ) def map_viewer( request: Request, + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], + tile_format: Annotated[ + Optional[ImageType], + Query( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", + ), + ] = None, + tile_scale: Annotated[ + int, + Query( + gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." + ), + ] = 1, + minzoom: Annotated[ + Optional[int], + Query(description="Overwrite default minzoom."), + ] = None, + maxzoom: Annotated[ + Optional[int], + Query(description="Overwrite default maxzoom."), + ] = None, src_path=Depends(self.path_dependency), - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), # noqa - tile_format: Optional[ImageType] = Query( - None, description="Output image type. Default is auto." - ), # noqa - tile_scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), # noqa - minzoom: Optional[int] = Query( - None, description="Overwrite default minzoom." - ), # noqa - maxzoom: Optional[int] = Query( - None, description="Overwrite default maxzoom." - ), # noqa - layer_params=Depends(self.layer_dependency), # noqa - dataset_params=Depends(self.dataset_dependency), # noqa - buffer: Optional[float] = Query( # noqa - None, - gt=0, - title="Tile buffer.", - description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", - ), - post_process=Depends(self.process_dependency), # noqa - rescale=Depends(self.rescale_dependency), # noqa - color_formula: Optional[str] = Query( # noqa - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), - colormap=Depends(self.colormap_dependency), # noqa - render_params=Depends(self.render_dependency), # noqa - reader_params=Depends(self.reader_dependency), # noqa - env=Depends(self.environment_dependency), # noqa + reader_params=Depends(self.reader_dependency), + tile_params=Depends(self.tile_dependency), + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), + post_process=Depends(self.process_dependency), + colormap=Depends(self.colormap_dependency), + render_params=Depends(self.render_dependency), + env=Depends(self.environment_dependency), ): """Return TileJSON document for a dataset.""" tilejson_url = self.url_for( - request, "tilejson", TileMatrixSetId=TileMatrixSetId + request, "tilejson", tileMatrixSetId=tileMatrixSetId ) + point_url = self.url_for(request, "point", lon="{lon}", lat="{lat}") if request.query_params._list: - tilejson_url += f"?{urlencode(request.query_params._list)}" + params = f"?{urlencode(request.query_params._list)}" + tilejson_url += params + point_url += params - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) return self.templates.TemplateResponse( + request, name="map.html", context={ - "request": request, "tilejson_endpoint": tilejson_url, + "point_endpoint": point_url, "tms": tms, - "resolutions": [tms._resolution(matrix) for matrix in tms], + "resolutions": [matrix.cellSize for matrix in tms], }, media_type="text/html", ) @@ -727,47 +1072,51 @@ def map_viewer( def wmts(self): # noqa: C901 """Register /wmts endpoint.""" - @self.router.get("/WMTSCapabilities.xml", response_class=XMLResponse) @self.router.get( - "/{TileMatrixSetId}/WMTSCapabilities.xml", response_class=XMLResponse + "/{tileMatrixSetId}/WMTSCapabilities.xml", + response_class=XMLResponse, + operation_id=f"{self.operation_prefix}getWMTS", ) def wmts( request: Request, - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Query( - self.default_tms, - description=f"TileMatrixSet Name (default: '{self.default_tms}')", - ), + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path( + description="Identifier selecting one of the TileMatrixSetId supported." + ), + ], + tile_format: Annotated[ + ImageType, + Query(description="Output image type. Default is png."), + ] = ImageType.png, + tile_scale: Annotated[ + int, + Query( + gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." + ), + ] = 1, + minzoom: Annotated[ + Optional[int], + Query(description="Overwrite default minzoom."), + ] = None, + maxzoom: Annotated[ + Optional[int], + Query(description="Overwrite default maxzoom."), + ] = None, + use_epsg: Annotated[ + bool, + Query( + description="Use EPSG code, not opengis.net, for the ows:SupportedCRS in the TileMatrixSet (set to True to enable ArcMap compatability)" + ), + ] = False, src_path=Depends(self.path_dependency), - tile_format: ImageType = Query( - ImageType.png, description="Output image type. Default is png." - ), - tile_scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - minzoom: Optional[int] = Query( - None, description="Overwrite default minzoom." - ), - maxzoom: Optional[int] = Query( - None, description="Overwrite default maxzoom." - ), - layer_params=Depends(self.layer_dependency), # noqa - dataset_params=Depends(self.dataset_dependency), # noqa - buffer: Optional[float] = Query( # noqa - None, - gt=0, - title="Tile buffer.", - description="Buffer on each side of the given tile. It must be a multiple of `0.5`. Output **tilesize** will be expanded to `tilesize + 2 * buffer` (e.g 0.5 = 257x257, 1.0 = 258x258).", - ), - post_process=Depends(self.process_dependency), # noqa - rescale=Depends(self.rescale_dependency), # noqa - color_formula: Optional[str] = Query( # noqa - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), - colormap=Depends(self.colormap_dependency), # noqa - render_params=Depends(self.render_dependency), # noqa reader_params=Depends(self.reader_dependency), + tile_params=Depends(self.tile_dependency), + layer_params=Depends(self.layer_dependency), + dataset_params=Depends(self.dataset_dependency), + post_process=Depends(self.process_dependency), + colormap=Depends(self.colormap_dependency), + render_params=Depends(self.render_dependency), env=Depends(self.environment_dependency), ): """OGC WMTS endpoint.""" @@ -777,7 +1126,7 @@ def wmts( "y": "{TileRow}", "scale": tile_scale, "format": tile_format.value, - "TileMatrixSetId": TileMatrixSetId, + "tileMatrixSetId": tileMatrixSetId, } tiles_url = self.url_for(request, "tile", **route_params) @@ -788,6 +1137,7 @@ def wmts( "minzoom", "maxzoom", "service", + "use_epsg", "request", ] qs = [ @@ -795,13 +1145,14 @@ def wmts( for (key, value) in request.query_params._list if key.lower() not in qs_key_to_remove ] - if qs: - tiles_url += f"?{urlencode(qs)}" - tms = self.supported_tms.get(TileMatrixSetId) + tms = self.supported_tms.get(tileMatrixSetId) with rasterio.Env(**env): - with self.reader(src_path, tms=tms, **reader_params) as src_dst: - bounds = src_dst.geographic_bounds + logger.info(f"opening data with reader: {self.reader}") + with self.reader( + src_path, tms=tms, **reader_params.as_dict() + ) as src_dst: + bounds = src_dst.get_geographic_bounds(tms.rasterio_geographic_crs) minzoom = minzoom if minzoom is not None else src_dst.minzoom maxzoom = maxzoom if maxzoom is not None else src_dst.maxzoom @@ -810,9 +1161,9 @@ def wmts( matrix = tms.matrix(zoom) tm = f""" - {matrix.identifier} + {matrix.id} {matrix.scaleDenominator} - {matrix.topLeftCorner[0]} {matrix.topLeftCorner[1]} + {matrix.pointOfOrigin[0]} {matrix.pointOfOrigin[1]} {matrix.tileWidth} {matrix.tileHeight} {matrix.matrixWidth} @@ -820,19 +1171,44 @@ def wmts( """ tileMatrix.append(tm) - return self.templates.TemplateResponse( - "wmts.xml", + if use_epsg: + supported_crs = f"EPSG:{tms.crs.to_epsg()}" + else: + supported_crs = tms.crs.srs + + layers = [ { - "request": request, - "tiles_endpoint": tiles_url, + "title": src_path if isinstance(src_path, str) else "TiTiler", + "name": "default", + "tiles_url": tiles_url, + "query_string": urlencode(qs, doseq=True) if qs else None, "bounds": bounds, + }, + ] + + bbox_crs_type = "WGS84BoundingBox" + bbox_crs_uri = "urn:ogc:def:crs:OGC:2:84" + if tms.rasterio_geographic_crs != WGS84_CRS: + bbox_crs_type = "BoundingBox" + bbox_crs_uri = CRS_to_urn(tms.rasterio_geographic_crs) + # WGS88BoundingBox is always xy ordered, but BoundingBox must match the CRS order + if crs_axis_inverted(tms.geographic_crs): + # match the bounding box coordinate order to the CRS + bounds = [bounds[1], bounds[0], bounds[3], bounds[2]] + + return self.templates.TemplateResponse( + request, + name="wmts.xml", + context={ + "tileMatrixSetId": tms.id, "tileMatrix": tileMatrix, - "tms": tms, - "title": "Cloud Optimized GeoTIFF", - "layer_name": "cogeo", + "supported_crs": supported_crs, + "bbox_crs_type": bbox_crs_type, + "bbox_crs_uri": bbox_crs_uri, + "layers": layers, "media_type": tile_format.mediatype, }, - media_type=MediaType.xml.value, + media_type="application/xml", ) ############################################################################ @@ -842,36 +1218,37 @@ def point(self): """Register /point endpoints.""" @self.router.get( - r"/point/{lon},{lat}", + "/point/{lon},{lat}", response_model=Point, response_class=JSONResponse, responses={200: {"description": "Return a value for a point"}}, + operation_id=f"{self.operation_prefix}getDataForPoint", ) def point( - lon: float = Path(..., description="Longitude"), - lat: float = Path(..., description="Latitude"), + lon: Annotated[float, Path(description="Longitude")], + lat: Annotated[float, Path(description="Latitude")], src_path=Depends(self.path_dependency), - coord_crs: Optional[CRS] = Depends(CoordCRSParams), + reader_params=Depends(self.reader_dependency), + coord_crs=Depends(CoordCRSParams), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Get Point value for a dataset.""" - with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: pts = src_dst.point( lon, lat, coord_crs=coord_crs or WGS84_CRS, - **layer_params, - **dataset_params, + **layer_params.as_dict(), + **dataset_params.as_dict(), ) return { "coordinates": [lon, lat], - "values": pts.data.tolist(), + "values": pts.array.tolist(), "band_names": pts.band_names, } @@ -881,36 +1258,47 @@ def point( def preview(self): """Register /preview endpoint.""" - @self.router.get(r"/preview", **img_endpoint_params) - @self.router.get(r"/preview.{format}", **img_endpoint_params) + @self.router.get( + "/preview", + operation_id=f"{self.operation_prefix}getPreview", + **img_endpoint_params, + ) + @self.router.get( + "/preview.{format}", + operation_id=f"{self.operation_prefix}getPreviewWithFormat", + **img_endpoint_params, + ) + @self.router.get( + "/preview/{width}x{height}.{format}", + operation_id=f"{self.operation_prefix}getPreviewWithSizeAndFormat", + **img_endpoint_params, + ) def preview( - format: ImageType = Query( - None, description="Output image type. Default is auto." - ), + format: Annotated[ + ImageType, + Field( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg)." + ), + ] = None, src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), layer_params=Depends(self.layer_dependency), - dst_crs: Optional[CRS] = Depends(DstCRSParams), dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), + image_params=Depends(self.img_preview_dependency), + dst_crs=Depends(DstCRSParams), post_process=Depends(self.process_dependency), - rescale=Depends(self.rescale_dependency), # noqa - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), colormap=Depends(self.colormap_dependency), render_params=Depends(self.render_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Create preview of a dataset.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: image = src_dst.preview( - **layer_params, - **image_params, - **dataset_params, + **layer_params.as_dict(), + **image_params.as_dict(exclude_none=False), + **dataset_params.as_dict(), dst_crs=dst_crs, ) dst_colormap = getattr(src_dst, "colormap", None) @@ -918,170 +1306,145 @@ def preview( if post_process: image = post_process(image) - if rescale: - image.rescale(rescale) - - if color_formula: - image.apply_color_formula(color_formula) - - if cmap := colormap or dst_colormap: - image = image.apply_colormap(cmap) - - if not format: - format = ImageType.jpeg if image.mask.all() else ImageType.png - - content = image.render( - img_format=format.driver, - **format.profile, - **render_params, + content, media_type = self.render_func( + image, + output_format=format, + colormap=colormap or dst_colormap, + **render_params.as_dict(), ) - return Response(content, media_type=format.mediatype) + return Response(content, media_type=media_type) ############################################################################ - # /crop (Optional) + # /bbox and /feature (Optional) ############################################################################ def part(self): # noqa: C901 - """Register /crop endpoint.""" + """Register /bbox and `/feature` endpoints.""" # GET endpoints @self.router.get( - r"/crop/{minx},{miny},{maxx},{maxy}.{format}", + "/bbox/{minx},{miny},{maxx},{maxy}.{format}", + operation_id=f"{self.operation_prefix}getDataForBoundingBoxWithFormat", **img_endpoint_params, ) @self.router.get( - r"/crop/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}", + "/bbox/{minx},{miny},{maxx},{maxy}/{width}x{height}.{format}", + operation_id=f"{self.operation_prefix}getDataForBoundingBoxWithSizesAndFormat", **img_endpoint_params, ) - def part( - minx: float = Path(..., description="Bounding box min X"), - miny: float = Path(..., description="Bounding box min Y"), - maxx: float = Path(..., description="Bounding box max X"), - maxy: float = Path(..., description="Bounding box max Y"), - format: ImageType = Query(..., description="Output image type."), + def bbox_image( + minx: Annotated[float, Path(description="Bounding box min X")], + miny: Annotated[float, Path(description="Bounding box min Y")], + maxx: Annotated[float, Path(description="Bounding box max X")], + maxy: Annotated[float, Path(description="Bounding box max Y")], + format: Annotated[ + ImageType, + Field( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", + ), + ] = None, src_path=Depends(self.path_dependency), - dst_crs: Optional[CRS] = Depends(DstCRSParams), - coord_crs: Optional[CRS] = Depends(CoordCRSParams), + reader_params=Depends(self.reader_dependency), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), + image_params=Depends(self.img_part_dependency), + dst_crs=Depends(DstCRSParams), + coord_crs=Depends(CoordCRSParams), post_process=Depends(self.process_dependency), - rescale=Depends(self.rescale_dependency), - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), colormap=Depends(self.colormap_dependency), render_params=Depends(self.render_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): - """Create image from part of a dataset.""" + """Create image from a bbox.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: image = src_dst.part( [minx, miny, maxx, maxy], dst_crs=dst_crs, bounds_crs=coord_crs or WGS84_CRS, - **layer_params, - **image_params, - **dataset_params, + **layer_params.as_dict(), + **image_params.as_dict(), + **dataset_params.as_dict(), ) dst_colormap = getattr(src_dst, "colormap", None) if post_process: image = post_process(image) - if rescale: - image.rescale(rescale) - - if color_formula: - image.apply_color_formula(color_formula) - - if cmap := colormap or dst_colormap: - image = image.apply_colormap(cmap) - - content = image.render( - img_format=format.driver, - **format.profile, - **render_params, + content, media_type = self.render_func( + image, + output_format=format, + colormap=colormap or dst_colormap, + **render_params.as_dict(), ) - return Response(content, media_type=format.mediatype) + return Response(content, media_type=media_type) # POST endpoints @self.router.post( - r"/crop", + "/feature", + operation_id=f"{self.operation_prefix}postDataForGeoJSON", **img_endpoint_params, ) @self.router.post( - r"/crop.{format}", + "/feature.{format}", + operation_id=f"{self.operation_prefix}postDataForGeoJSONWithFormat", **img_endpoint_params, ) @self.router.post( - r"/crop/{width}x{height}.{format}", + "/feature/{width}x{height}.{format}", + operation_id=f"{self.operation_prefix}postDataForGeoJSONWithSizesAndFormat", **img_endpoint_params, ) - def geojson_crop( - geojson: Feature = Body(..., description="GeoJSON Feature."), - format: ImageType = Query( - None, description="Output image type. Default is auto." - ), + def feature_image( + geojson: Annotated[Feature, Body(description="GeoJSON Feature.")], + format: Annotated[ + ImageType, + Field( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg)." + ), + ] = None, src_path=Depends(self.path_dependency), - coord_crs: Optional[CRS] = Depends(CoordCRSParams), + reader_params=Depends(self.reader_dependency), layer_params=Depends(self.layer_dependency), dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), + image_params=Depends(self.img_part_dependency), + coord_crs=Depends(CoordCRSParams), + dst_crs=Depends(DstCRSParams), post_process=Depends(self.process_dependency), - rescale=Depends(self.rescale_dependency), - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), colormap=Depends(self.colormap_dependency), render_params=Depends(self.render_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Create image from a geojson feature.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: image = src_dst.feature( - geojson.dict(exclude_none=True), + geojson.model_dump(exclude_none=True), shape_crs=coord_crs or WGS84_CRS, - **layer_params, - **image_params, - **dataset_params, + dst_crs=dst_crs, + **layer_params.as_dict(), + **image_params.as_dict(), + **dataset_params.as_dict(), ) dst_colormap = getattr(src_dst, "colormap", None) if post_process: image = post_process(image) - if rescale: - image.rescale(rescale) - - if color_formula: - image.apply_color_formula(color_formula) - - if cmap := colormap or dst_colormap: - image = image.apply_colormap(cmap) - - if not format: - format = ImageType.jpeg if image.mask.all() else ImageType.png - - content = image.render( - img_format=format.driver, - **format.profile, - **render_params, + content, media_type = self.render_func( + image, + output_format=format, + colormap=colormap or dst_colormap, + **render_params.as_dict(), ) - return Response(content, media_type=format.mediatype) + return Response(content, media_type=media_type) -@dataclass +@define(kw_only=True) class MultiBaseTilerFactory(TilerFactory): """Custom Tiler Factory for MultiBaseReader classes. @@ -1118,17 +1481,19 @@ def info(self): "description": "Return dataset's basic info or the list of available assets." } }, + operation_id=f"{self.operation_prefix}getInfo", ) def info( src_path=Depends(self.path_dependency), - asset_params=Depends(self.assets_dependency), reader_params=Depends(self.reader_dependency), + asset_params=Depends(self.assets_dependency), env=Depends(self.environment_dependency), ): """Return dataset's basic info or the list of available assets.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - return src_dst.info(**asset_params) + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + return src_dst.info(**asset_params.as_dict()) @self.router.get( "/info.geojson", @@ -1141,31 +1506,34 @@ def info( "description": "Return dataset's basic info as a GeoJSON feature.", } }, + operation_id=f"{self.operation_prefix}getInfoGeoJSON", ) def info_geojson( src_path=Depends(self.path_dependency), - asset_params=Depends(self.assets_dependency), reader_params=Depends(self.reader_dependency), + asset_params=Depends(self.assets_dependency), + crs=Depends(CRSParams), env=Depends(self.environment_dependency), ): """Return dataset's basic info as a GeoJSON feature.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS) + geometry = bounds_to_geometry(bounds) + return Feature( type="Feature", - geometry=Polygon.from_bounds(*src_dst.geographic_bounds), - properties={ - asset: asset_info - for asset, asset_info in src_dst.info( - **asset_params - ).items() - }, + bbox=bounds, + geometry=geometry, + properties=src_dst.info(**asset_params.as_dict()), ) @self.router.get( "/assets", response_model=List[str], responses={200: {"description": "Return a list of supported assets."}}, + operation_id=f"{self.operation_prefix}getAssets", ) def available_assets( src_path=Depends(self.path_dependency), @@ -1174,7 +1542,8 @@ def available_assets( ): """Return a list of supported assets.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: return src_dst.assets # Overwrite the `/statistics` endpoint because the MultiBaseReader output model is different (Dict[str, Dict[str, BandStatistics]]) @@ -1193,26 +1562,28 @@ def statistics(self): # noqa: C901 "description": "Return dataset's statistics.", } }, + operation_id=f"{self.operation_prefix}getAssetsStatistics", ) def asset_statistics( src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), asset_params=Depends(AssetsBidxParams), dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), + image_params=Depends(self.img_preview_dependency), stats_params=Depends(self.stats_dependency), histogram_params=Depends(self.histogram_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Per Asset statistics""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: return src_dst.statistics( - **asset_params, - **image_params, - **dataset_params, - **stats_params, - hist_options={**histogram_params}, + **asset_params.as_dict(), + **image_params.as_dict(exclude_none=False), + **dataset_params.as_dict(), + **stats_params.as_dict(), + hist_options=histogram_params.as_dict(), ) # MultiBaseReader merged statistics @@ -1228,30 +1599,39 @@ def asset_statistics( "description": "Return dataset's statistics.", } }, + operation_id=f"{self.operation_prefix}getStatistics", ) def statistics( src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), layer_params=Depends(AssetsBidxExprParamsOptional), dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), + image_params=Depends(self.img_preview_dependency), + post_process=Depends(self.process_dependency), stats_params=Depends(self.stats_dependency), histogram_params=Depends(self.histogram_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Merged assets statistics.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: # Default to all available assets if not layer_params.assets and not layer_params.expression: layer_params.assets = src_dst.assets - return src_dst.merged_statistics( - **layer_params, - **image_params, - **dataset_params, - **stats_params, - hist_options={**histogram_params}, + image = src_dst.preview( + **layer_params.as_dict(), + **image_params.as_dict(), + **dataset_params.as_dict(), + ) + + if post_process: + image = post_process(image) + + return image.statistics( + **stats_params.as_dict(), + hist_options=histogram_params.as_dict(), ) # POST endpoint @@ -1262,23 +1642,27 @@ def statistics( response_class=GeoJSONResponse, responses={ 200: { - "content": {"application/json": {}}, - "description": "Return dataset's statistics.", + "content": {"application/geo+json": {}}, + "description": "Return dataset's statistics from feature or featureCollection.", } }, + operation_id=f"{self.operation_prefix}postStatisticsForGeoJSON", ) def geojson_statistics( - geojson: Union[FeatureCollection, Feature] = Body( - ..., description="GeoJSON Feature or FeatureCollection." - ), + geojson: Annotated[ + Union[FeatureCollection, Feature], + Body(description="GeoJSON Feature or FeatureCollection."), + ], src_path=Depends(self.path_dependency), - coord_crs: Optional[CRS] = Depends(CoordCRSParams), + reader_params=Depends(self.reader_dependency), layer_params=Depends(AssetsBidxExprParamsOptional), dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), + coord_crs=Depends(CoordCRSParams), + dst_crs=Depends(DstCRSParams), + post_process=Depends(self.process_dependency), + image_params=Depends(self.img_part_dependency), stats_params=Depends(self.stats_dependency), histogram_params=Depends(self.histogram_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Get Statistics from a geojson feature or featureCollection.""" @@ -1287,42 +1671,40 @@ def geojson_statistics( fc = FeatureCollection(type="FeatureCollection", features=[geojson]) with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: # Default to all available assets if not layer_params.assets and not layer_params.expression: layer_params.assets = src_dst.assets - for feature in fc: - data = src_dst.feature( - feature.dict(exclude_none=True), + for feature in fc.features: + image = src_dst.feature( + feature.model_dump(exclude_none=True), shape_crs=coord_crs or WGS84_CRS, - **layer_params, - **image_params, - **dataset_params, + dst_crs=dst_crs, + align_bounds_with_dataset=True, + **layer_params.as_dict(), + **image_params.as_dict(), + **dataset_params.as_dict(), ) - stats = get_array_statistics( - data.as_masked(), - **stats_params, - **histogram_params, + if post_process: + image = post_process(image) + + stats = image.statistics( + **stats_params.as_dict(), + hist_options=histogram_params.as_dict(), ) - feature.properties = feature.properties or {} - feature.properties.update( - { - # NOTE: because we use `src_dst.feature` the statistics will be in form of - # `Dict[str, BandStatistics]` and not `Dict[str, Dict[str, BandStatistics]]` - "statistics": { - f"{data.band_names[ix]}": BandStatistics(**stats[ix]) - for ix in range(len(stats)) - } - } - ) + feature.properties = feature.properties or {} + # NOTE: because we use `src_dst.feature` the statistics will be in form of + # `Dict[str, BandStatistics]` and not `Dict[str, Dict[str, BandStatistics]]` + feature.properties.update({"statistics": stats}) return fc.features[0] if isinstance(geojson, Feature) else fc -@dataclass +@define(kw_only=True) class MultiBandTilerFactory(TilerFactory): """Custom Tiler Factory for MultiBandReader classes. @@ -1356,17 +1738,19 @@ def info(self): response_model_exclude_none=True, response_class=JSONResponse, responses={200: {"description": "Return dataset's basic info."}}, + operation_id=f"{self.operation_prefix}getInfo", ) def info( src_path=Depends(self.path_dependency), - bands_params=Depends(self.bands_dependency), reader_params=Depends(self.reader_dependency), + bands_params=Depends(self.bands_dependency), env=Depends(self.environment_dependency), ): """Return dataset's basic info.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - return src_dst.info(**bands_params) + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + return src_dst.info(**bands_params.as_dict()) @self.router.get( "/info.geojson", @@ -1379,26 +1763,34 @@ def info( "description": "Return dataset's basic info as a GeoJSON feature.", } }, + operation_id=f"{self.operation_prefix}getInfoGeoJSON", ) def info_geojson( src_path=Depends(self.path_dependency), - bands_params=Depends(self.bands_dependency), reader_params=Depends(self.reader_dependency), + bands_params=Depends(self.bands_dependency), + crs=Depends(CRSParams), env=Depends(self.environment_dependency), ): """Return dataset's basic info as a GeoJSON feature.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + bounds = src_dst.get_geographic_bounds(crs or WGS84_CRS) + geometry = bounds_to_geometry(bounds) + return Feature( type="Feature", - geometry=Polygon.from_bounds(*src_dst.geographic_bounds), - properties=src_dst.info(**bands_params), + bbox=bounds, + geometry=geometry, + properties=src_dst.info(**bands_params.as_dict()), ) @self.router.get( "/bands", response_model=List[str], responses={200: {"description": "Return a list of supported bands."}}, + operation_id=f"{self.operation_prefix}getBands", ) def available_bands( src_path=Depends(self.path_dependency), @@ -1407,7 +1799,8 @@ def available_bands( ): """Return a list of supported bands.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: return src_dst.bands # Overwrite the `/statistics` endpoint because we need bands to default to the list of bands. @@ -1425,26 +1818,39 @@ def statistics(self): # noqa: C901 "description": "Return dataset's statistics.", } }, + operation_id=f"{self.operation_prefix}getStatistics", ) def statistics( src_path=Depends(self.path_dependency), + reader_params=Depends(self.reader_dependency), bands_params=Depends(BandsExprParamsOptional), dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), + image_params=Depends(self.img_preview_dependency), + post_process=Depends(self.process_dependency), stats_params=Depends(self.stats_dependency), histogram_params=Depends(self.histogram_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Get Dataset statistics.""" with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - return src_dst.statistics( - **bands_params, - **image_params, - **dataset_params, - **stats_params, - hist_options={**histogram_params}, + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: + # Default to all available bands + if not bands_params.bands and not bands_params.expression: + bands_params.bands = src_dst.bands + + image = src_dst.preview( + **bands_params.as_dict(), + **image_params.as_dict(), + **dataset_params.as_dict(), + ) + + if post_process: + image = post_process(image) + + return image.statistics( + **stats_params.as_dict(), + hist_options=histogram_params.as_dict(), ) # POST endpoint @@ -1455,23 +1861,27 @@ def statistics( response_class=GeoJSONResponse, responses={ 200: { - "content": {"application/json": {}}, - "description": "Return dataset's statistics.", + "content": {"application/geo+json": {}}, + "description": "Return dataset's statistics from feature or featureCollection.", } }, + operation_id=f"{self.operation_prefix}postStatisticsForGeoJSON", ) def geojson_statistics( - geojson: Union[FeatureCollection, Feature] = Body( - ..., description="GeoJSON Feature or FeatureCollection." - ), + geojson: Annotated[ + Union[FeatureCollection, Feature], + Body(description="GeoJSON Feature or FeatureCollection."), + ], src_path=Depends(self.path_dependency), - coord_crs: Optional[CRS] = Depends(CoordCRSParams), + reader_params=Depends(self.reader_dependency), bands_params=Depends(BandsExprParamsOptional), dataset_params=Depends(self.dataset_dependency), - image_params=Depends(self.img_dependency), + image_params=Depends(self.img_part_dependency), + coord_crs=Depends(CoordCRSParams), + dst_crs=Depends(DstCRSParams), + post_process=Depends(self.process_dependency), stats_params=Depends(self.stats_dependency), histogram_params=Depends(self.histogram_dependency), - reader_params=Depends(self.reader_dependency), env=Depends(self.environment_dependency), ): """Get Statistics from a geojson feature or featureCollection.""" @@ -1480,136 +1890,136 @@ def geojson_statistics( fc = FeatureCollection(type="FeatureCollection", features=[geojson]) with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: + logger.info(f"opening data with reader: {self.reader}") + with self.reader(src_path, **reader_params.as_dict()) as src_dst: # Default to all available bands if not bands_params.bands and not bands_params.expression: bands_params.bands = src_dst.bands - for feature in fc: - data = src_dst.feature( - feature.dict(exclude_none=True), + for feature in fc.features: + image = src_dst.feature( + feature.model_dump(exclude_none=True), shape_crs=coord_crs or WGS84_CRS, - **bands_params, - **image_params, - **dataset_params, + dst_crs=dst_crs, + align_bounds_with_dataset=True, + **bands_params.as_dict(), + **image_params.as_dict(), + **dataset_params.as_dict(), ) - stats = get_array_statistics( - data.as_masked(), - **stats_params, - **histogram_params, + + if post_process: + image = post_process(image) + + stats = image.statistics( + **stats_params.as_dict(), + hist_options=histogram_params.as_dict(), ) feature.properties = feature.properties or {} - feature.properties.update( - { - "statistics": { - f"{data.band_names[ix]}": BandStatistics( - **stats[ix] - ) - for ix in range(len(stats)) - } - } - ) + feature.properties.update({"statistics": stats}) return fc.features[0] if isinstance(geojson, Feature) else fc -@dataclass -class TMSFactory: +@define(kw_only=True) +class TMSFactory(BaseFactory): """TileMatrixSet endpoints Factory.""" supported_tms: TileMatrixSets = morecantile_tms - # FastAPI router - router: APIRouter = field(default_factory=APIRouter) - - # Router Prefix is needed to find the path for /tile if the TilerFactory.router is mounted - # with other router (multiple `.../tile` routes). - # e.g if you mount the route with `/cog` prefix, set router_prefix to cog and - router_prefix: str = "" - - def __post_init__(self): - """Post Init: register route and configure specific options.""" - self.register_routes() - - def url_for(self, request: Request, name: str, **path_params: Any) -> str: - """Return full url (with prefix) for a specific endpoint.""" - url_path = self.router.url_path_for(name, **path_params) - base_url = str(request.base_url) - if self.router_prefix: - base_url += self.router_prefix.lstrip("/") - - return str(url_path.make_absolute_url(base_url=base_url)) - def register_routes(self): """Register TMS endpoint routes.""" @self.router.get( - r"/tileMatrixSets", + "/tileMatrixSets", response_model=TileMatrixSetList, response_model_exclude_none=True, summary="Retrieve the list of available tiling schemes (tile matrix sets).", - operation_id="getTileMatrixSetsList", + operation_id=f"{self.operation_prefix}getTileMatrixSetsList", + responses={ + 200: { + "content": { + "application/json": {}, + }, + }, + }, ) - async def TileMatrixSet_list(request: Request): + async def tilematrixsets(request: Request): """ OGC Specification: http://docs.opengeospatial.org/per/19-069.html#_tilematrixsets """ - return { - "tileMatrixSets": [ + data = TileMatrixSetList( + tileMatrixSets=[ { - "id": tms, - "title": tms, + "id": tms_id, "links": [ { "href": self.url_for( request, - "TileMatrixSet_info", - TileMatrixSetId=tms, + "tilematrixset", + tileMatrixSetId=tms_id, ), - "rel": "item", + "rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme", "type": "application/json", + "title": f"Definition of {tms_id} tileMatrixSet", } ], } - for tms in self.supported_tms.list() + for tms_id in self.supported_tms.list() ] - } + ) + + return data @self.router.get( - r"/tileMatrixSets/{TileMatrixSetId}", + "/tileMatrixSets/{tileMatrixSetId}", response_model=TileMatrixSet, response_model_exclude_none=True, summary="Retrieve the definition of the specified tiling scheme (tile matrix set).", - operation_id="getTileMatrixSet", + operation_id=f"{self.operation_prefix}getTileMatrixSet", + responses={ + 200: { + "content": { + "application/json": {}, + }, + }, + }, ) - async def TileMatrixSet_info( - TileMatrixSetId: Literal[tuple(self.supported_tms.list())] = Path( - ..., description="TileMatrixSet Name." - ) + async def tilematrixset( + request: Request, + tileMatrixSetId: Annotated[ + Literal[tuple(self.supported_tms.list())], + Path(description="Identifier for a supported TileMatrixSet."), + ], ): """ OGC Specification: http://docs.opengeospatial.org/per/19-069.html#_tilematrixset """ - return self.supported_tms.get(TileMatrixSetId) + return self.supported_tms.get(tileMatrixSetId) -@dataclass -class AlgorithmFactory: +@define(kw_only=True) +class AlgorithmFactory(BaseFactory): """Algorithm endpoints Factory.""" # Supported algorithm supported_algorithm: Algorithms = available_algorithms - # FastAPI router - router: APIRouter = field(default_factory=APIRouter) - - def __post_init__(self): - """Post Init: register routes""" + def register_routes(self): + """Register Algorithm routes.""" def metadata(algorithm: BaseAlgorithm) -> AlgorithmMetadata: """Algorithm Metadata""" - props = algorithm.schema()["properties"] + props = algorithm.model_json_schema()["properties"] + + # title and description + info = { + k: v["default"] + for k, v in props.items() + if k == "title" or k == "description" + } + title = info.get("title", None) + description = info.get("description", None) # Inputs Metadata ins = { @@ -1629,15 +2039,24 @@ def metadata(algorithm: BaseAlgorithm) -> AlgorithmMetadata: params = { k: v for k, v in props.items() - if not k.startswith("input_") and not k.startswith("output_") + if not k.startswith("input_") + and not k.startswith("output_") + and k != "title" + and k != "description" } - return AlgorithmMetadata(inputs=ins, outputs=outs, parameters=params) + return AlgorithmMetadata( + title=title, + description=description, + inputs=ins, + outputs=outs, + parameters=params, + ) @self.router.get( "/algorithms", response_model=Dict[str, AlgorithmMetadata], summary="Retrieve the list of available Algorithms.", - operation_id="getAlgorithms", + operation_id=f"{self.operation_prefix}getAlgorithmList", ) def available_algorithms(request: Request): """Retrieve the list of available Algorithms.""" @@ -1647,12 +2066,177 @@ def available_algorithms(request: Request): "/algorithms/{algorithmId}", response_model=AlgorithmMetadata, summary="Retrieve the metadata of the specified algorithm.", - operation_id="getAlgorithm", + operation_id=f"{self.operation_prefix}getAlgorithm", ) def algorithm_metadata( - algorithm: Literal[tuple(self.supported_algorithm.list())] = Path( - ..., description="Algorithm name", alias="algorithmId" - ), + algorithm: Annotated[ + Literal[tuple(self.supported_algorithm.list())], + Path(description="Algorithm name", alias="algorithmId"), + ], ): """Retrieve the metadata of the specified algorithm.""" return metadata(self.supported_algorithm.get(algorithm)) + + +@define(kw_only=True) +class ColorMapFactory(BaseFactory): + """Colormap endpoints Factory.""" + + # Supported colormaps + supported_colormaps: ColorMaps = default_cmap + + def register_routes(self): # noqa: C901 + """Register ColorMap routes.""" + + @self.router.get( + "/colorMaps", + response_model=ColorMapsList, + response_model_exclude_none=True, + summary="Retrieve the list of available colormaps.", + operation_id=f"{self.operation_prefix}getColorMapList", + ) + def available_colormaps(request: Request): + """Retrieve the list of available colormaps.""" + return { + "colorMaps": self.supported_colormaps.list(), + "links": [ + { + "title": "List of available colormaps", + "href": self.url_for( + request, + "available_colormaps", + ), + "type": "application/json", + "rel": "self", + }, + { + "title": "Retrieve colorMap metadata", + "href": self.url_for( + request, "colormap_metadata", colorMapId="{colorMapId}" + ), + "type": "application/json", + "rel": "data", + "templated": True, + }, + { + "title": "Retrieve colorMap as image", + "href": self.url_for( + request, "colormap_metadata", colorMapId="{colorMapId}" + ) + + "?format=png", + "type": "image/png", + "rel": "data", + "templated": True, + }, + ], + } + + @self.router.get( + "/colorMaps/{colorMapId}", + response_model=ColorMapType, + summary="Retrieve the colorMap metadata or image.", + operation_id=f"{self.operation_prefix}getColorMap", + responses={ + 200: { + "content": { + "application/json": {}, + "image/png": {}, + "image/jpeg": {}, + "image/jpg": {}, + "image/webp": {}, + "image/jp2": {}, + "image/tiff; application=geotiff": {}, + "application/x-binary": {}, + } + }, + }, + ) + def colormap_metadata( + colormap: Annotated[ + Literal[tuple(self.supported_colormaps.list())], + Path(description="ColorMap name", alias="colorMapId"), + ], + # Image Output Options + format: Annotated[ + Optional[ImageType], + Query( + description="Return colorMap as Image.", + ), + ] = None, + orientation: Annotated[ + Optional[Literal["vertical", "horizontal"]], + Query( + description="Image Orientation.", + ), + ] = None, + height: Annotated[ + Optional[int], + Query( + description="Image Height (default to 20px for horizontal or 256px for vertical).", + ), + ] = None, + width: Annotated[ + Optional[int], + Query( + description="Image Width (default to 256px for horizontal or 20px for vertical).", + ), + ] = None, + ): + """Retrieve the metadata of the specified colormap.""" + cmap = self.supported_colormaps.get(colormap) + + if format: + ############################################################### + # SEQUENCE CMAP + if isinstance(cmap, Sequence): + values = [minv for ((minv, _), _) in cmap] + arr = numpy.array([values] * 20) + + if orientation == "vertical": + height = height or 256 if len(values) < 256 else len(values) + else: + width = width or 256 if len(values) < 256 else len(values) + + ############################################################### + # DISCRETE CMAP + elif len(cmap) != 256 or max(cmap) >= 256 or min(cmap) < 0: + values = list(cmap) + arr = numpy.array([values] * 20) + + if orientation == "vertical": + height = height or 256 if len(values) < 256 else len(values) + else: + width = width or 256 if len(values) < 256 else len(values) + + ############################################################### + # LINEAR CMAP + else: + cmin, cmax = min(cmap), max(cmap) + arr = numpy.array( + [ + numpy.round(numpy.linspace(cmin, cmax, num=256)).astype( + numpy.uint8 + ) + ] + * 20 + ) + + if orientation == "vertical": + arr = arr.transpose([1, 0]) + + img = ImageData(arr) + + width = width or img.width + height = height or img.height + if width != img.width or height != img.height: + img = img.resize(height, width) + + return Response( + img.render(img_format=format.driver, colormap=cmap), + media_type=format.mediatype, + ) + + if isinstance(cmap, Sequence): + return [(k, numpy.array(v).tolist()) for (k, v) in cmap] + else: + return {k: numpy.array(v).tolist() for k, v in cmap.items()} diff --git a/src/titiler/core/titiler/core/middleware.py b/src/titiler/core/titiler/core/middleware.py index f592c962d..49ae11344 100644 --- a/src/titiler/core/titiler/core/middleware.py +++ b/src/titiler/core/titiler/core/middleware.py @@ -1,38 +1,36 @@ """Titiler middlewares.""" +from __future__ import annotations + import logging import re import time -from typing import Optional, Set +from dataclasses import dataclass, field +from typing import Set +from urllib.parse import urlencode -from fastapi.logger import logger from starlette.datastructures import MutableHeaders from starlette.requests import Request from starlette.types import ASGIApp, Message, Receive, Scope, Send +from titiler.core import telemetry + +@dataclass(frozen=True) class CacheControlMiddleware: - """MiddleWare to add CacheControl in response headers.""" - - def __init__( - self, - app: ASGIApp, - cachecontrol: Optional[str] = None, - cachecontrol_max_http_code: Optional[int] = 500, - exclude_path: Optional[Set[str]] = None, - ) -> None: - """Init Middleware. - - Args: - app (ASGIApp): starlette/FastAPI application. - cachecontrol (str): Cache-Control string to add to the response. - exclude_path (set): Set of regex expression to use to filter the path. - - """ - self.app = app - self.cachecontrol = cachecontrol - self.cachecontrol_max_http_code = cachecontrol_max_http_code - self.exclude_path = exclude_path or set() + """MiddleWare to add CacheControl in response headers. + + Args: + app (ASGIApp): starlette/FastAPI application. + cachecontrol (str): Cache-Control string to add to the response. + exclude_path (set): Set of regex expression to use to filter the path. + + """ + + app: ASGIApp + cachecontrol: str | None = None + cachecontrol_max_http_code: int = 500 + exclude_path: Set[str] = field(default_factory=set) async def __call__(self, scope: Scope, receive: Receive, send: Send): """Handle call.""" @@ -49,10 +47,7 @@ async def send_wrapper(message: Message): scope["method"] in ["HEAD", "GET"] and message["status"] < self.cachecontrol_max_http_code and not any( - [ - re.match(path, scope["path"]) - for path in self.exclude_path - ] + re.match(path, scope["path"]) for path in self.exclude_path ) ): response_headers["Cache-Control"] = self.cachecontrol @@ -62,17 +57,16 @@ async def send_wrapper(message: Message): await self.app(scope, receive, send_wrapper) +@dataclass(frozen=True) class TotalTimeMiddleware: - """MiddleWare to add Total process time in response headers.""" + """MiddleWare to add Total process time in response headers. - def __init__(self, app: ASGIApp) -> None: - """Init Middleware. + Args: + app (ASGIApp): starlette/FastAPI application. - Args: - app (ASGIApp): starlette/FastAPI application. + """ - """ - self.app = app + app: ASGIApp async def __call__(self, scope: Scope, receive: Receive, send: Send): """Handle call.""" @@ -84,6 +78,8 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send): async def send_wrapper(message: Message): """Send Message.""" + nonlocal start_time + if message["type"] == "http.response.start": response_headers = MutableHeaders(scope=message) process_time = time.time() - start_time @@ -99,70 +95,82 @@ async def send_wrapper(message: Message): await self.app(scope, receive, send_wrapper) +@dataclass(frozen=True) class LoggerMiddleware: """MiddleWare to add logging.""" - def __init__( - self, - app: ASGIApp, - querystrings: bool = False, - headers: bool = False, - ) -> None: - """Init Middleware. - - Args: - app (ASGIApp): starlette/FastAPI application. - - """ - self.app = app - self.querystrings = querystrings - self.headers = headers - self.logger = logger - logger.setLevel(logging.DEBUG) + app: ASGIApp + logger: logging.Logger = field( + default_factory=lambda: logging.getLogger("titiler.requests") + ) async def __call__(self, scope: Scope, receive: Receive, send: Send): """Handle call.""" - if scope["type"] == "http": - request = Request(scope) - - self.logger.debug(str(request.url)) + if scope["type"] != "http": + return await self.app(scope, receive, send) + + request = Request(scope, receive=receive) + data = { + "http.method": request.method, + "http.url": str(request.url), + "http.scheme": request.url.scheme, + "http.host": request.headers.get("host", request.url.hostname or "unknown"), + "http.target": request.url.path + + (f"?{request.url.query}" if request.url.query else ""), + "http.user_agent": request.headers.get("user-agent"), + "http.referer": next( + (request.headers.get(attr) for attr in ["referer", "referrer"]), + None, + ), + "http.request.header.content-length": request.headers.get("content-length"), + "http.request.header.accept-encoding": request.headers.get( + "accept-encoding" + ), + "http.request.header.origin": request.headers.get("origin"), + "net.host.name": request.url.hostname, + "net.host.port": request.url.port, + "titiler.query_params": dict(request.query_params), + } + + telemetry.add_span_attributes(telemetry.flatten_dict(data)) + + exception: Exception | None = None + try: + await self.app(scope, receive, send) + except Exception as e: + exception = e - qs = dict(request.query_params) - if qs and self.querystrings: - self.logger.debug(qs) + if route := scope.get("route"): + data["http.route"] = route.path - if self.headers: - self.logger.debug(dict(request.headers)) + data["titiler.path_params"] = request.path_params - await self.app(scope, receive, send) + self.logger.info( + f"Request received: {request.url.path} {request.method}", + extra=data, + ) + if exception: + raise exception +@dataclass(frozen=True) class LowerCaseQueryStringMiddleware: """Middleware to make URL parameters case-insensitive. taken from: https://github.com/tiangolo/fastapi/issues/826 - """ - def __init__(self, app: ASGIApp) -> None: - """Init Middleware. - - Args: - app (ASGIApp): starlette/FastAPI application. + """ - """ - self.app = app + app: ASGIApp async def __call__(self, scope: Scope, receive: Receive, send: Send): """Handle call.""" if scope["type"] == "http": request = Request(scope) - DECODE_FORMAT = "latin-1" - - query_string = "" - for k, v in request.query_params.multi_items(): - query_string += k.lower() + "=" + v + "&" - - query_string = query_string[:-1] + query_items = [ + (k.lower(), v) for k, v in request.query_params.multi_items() + ] + query_string = urlencode(query_items, doseq=True) request.scope["query_string"] = query_string.encode(DECODE_FORMAT) await self.app(scope, receive, send) diff --git a/src/titiler/core/titiler/core/models/OGC.py b/src/titiler/core/titiler/core/models/OGC.py index 6b735dff2..39343bf86 100644 --- a/src/titiler/core/titiler/core/models/OGC.py +++ b/src/titiler/core/titiler/core/models/OGC.py @@ -1,9 +1,13 @@ """OGC models.""" +from datetime import datetime +from typing import Dict, List, Literal, Optional, Set, Union -from typing import List +from morecantile.models import CRSType +from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, RootModel +from typing_extensions import Annotated -from pydantic import AnyHttpUrl, BaseModel +from titiler.core.resources.enums import MediaType class TileMatrixSetLink(BaseModel): @@ -18,22 +22,16 @@ class TileMatrixSetLink(BaseModel): rel: str = "item" type: str = "application/json" - class Config: - """Config for model.""" - - use_enum_values = True - class TileMatrixSetRef(BaseModel): """ TileMatrixSetRef model. Based on http://docs.opengeospatial.org/per/19-069.html#_tilematrixsets - """ id: str - title: str + title: Optional[str] = None links: List[TileMatrixSetLink] @@ -42,7 +40,715 @@ class TileMatrixSetList(BaseModel): TileMatrixSetList model. Based on http://docs.opengeospatial.org/per/19-069.html#_tilematrixsets - """ tileMatrixSets: List[TileMatrixSetRef] + + +class Link(BaseModel): + """Link model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/common-core/link.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + href: Annotated[ + str, + Field( + json_schema_extra={ + "description": "Supplies the URI to a remote resource (or resource fragment).", + "examples": ["http://data.example.com/buildings/123"], + } + ), + ] + rel: Annotated[ + str, + Field( + json_schema_extra={ + "description": "The type or semantics of the relation.", + "examples": ["alternate"], + } + ), + ] + type: Annotated[ + Optional[MediaType], + Field( + json_schema_extra={ + "description": "A hint indicating what the media type of the result of dereferencing the link should be.", + "examples": ["application/geo+json"], + } + ), + ] = None + templated: Annotated[ + Optional[bool], + Field( + json_schema_extra={ + "description": "This flag set to true if the link is a URL template.", + } + ), + ] = None + varBase: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "A base path to retrieve semantic information about the variables used in URL template.", + "examples": ["/ogcapi/vars/"], + } + ), + ] = None + hreflang: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "A hint indicating what the language of the result of dereferencing the link should be.", + "examples": ["en"], + } + ), + ] = None + title: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Used to label the destination of a link such that it can be used as a human-readable identifier.", + "examples": ["Trierer Strasse 70, 53115 Bonn"], + } + ), + ] = None + length: Optional[int] = None + + model_config = {"use_enum_values": True} + + +class TimeStamp(RootModel): + """TimeStamp model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/common-geodata/timeStamp.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + root: Annotated[ + datetime, + Field( + json_schema_extra={ + "description": "This property indicates the time and date when the response was generated using RFC 3339 notation.", + "examples": ["2017-08-17T08:05:32Z"], + } + ), + ] + + +class BoundingBox(BaseModel): + """BoundingBox model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/2DBoundingBox.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + lowerLeft: Annotated[ + List[float], + Field( + max_length=2, + min_length=2, + json_schema_extra={ + "description": "A 2D Point in the CRS indicated elsewhere", + }, + ), + ] + upperRight: Annotated[ + List[float], + Field( + max_length=2, + min_length=2, + json_schema_extra={ + "description": "A 2D Point in the CRS indicated elsewhere", + }, + ), + ] + crs: Annotated[Optional[CRSType], Field(json_schema_extra={"title": "CRS"})] = None + orderedAxes: Annotated[Optional[List[str]], Field(max_length=2, min_length=2)] = ( + None + ) + + +# Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/propertiesSchema.yaml +Type = Literal["array", "boolean", "integer", "null", "number", "object", "string"] + +# Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/propertiesSchema.yaml +AccessConstraints = Literal[ + "unclassified", "restricted", "confidential", "secret", "topSecret" +] + + +class Properties(BaseModel): + """Properties model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/propertiesSchema.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + title: Optional[str] = None + description: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Implements 'description'", + } + ), + ] = None + type: Optional[Type] = None + enum: Annotated[ + Optional[Set], + Field( + min_length=1, + json_schema_extra={ + "description": "Implements 'acceptedValues'", + }, + ), + ] = None + format: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Complements implementation of 'type'", + } + ), + ] = None + contentMediaType: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Implements 'mediaType'", + } + ), + ] = None + maximum: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Implements 'range'", + } + ), + ] = None + exclusiveMaximum: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Implements 'range'", + } + ), + ] = None + minimum: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Implements 'range'", + } + ), + ] = None + exclusiveMinimum: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Implements 'range'", + } + ), + ] = None + pattern: Optional[str] = None + maxItems: Annotated[ + Optional[int], + Field( + ge=0, + json_schema_extra={ + "description": "Implements 'upperMultiplicity'", + }, + ), + ] = None + minItems: Annotated[ + Optional[int], + Field( + ge=0, + json_schema_extra={ + "description": "Implements 'lowerMultiplicity'", + }, + ), + ] = 0 + observedProperty: Optional[str] = None + observedPropertyURI: Optional[AnyUrl] = None + uom: Optional[str] = None + uomURI: Optional[AnyUrl] = None + + +class PropertiesSchema(BaseModel): + """PropertiesSchema model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/propertiesSchema.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + type: Literal["object"] + required: Annotated[ + Optional[List[str]], + Field( + min_length=1, + json_schema_extra={ + "description": "Implements 'multiplicity' by citing property 'name' defined as 'additionalProperties'", + }, + ), + ] = None + properties: Dict[str, Properties] + + +class Style(BaseModel): + """Style model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/style.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + id: Annotated[ + str, + Field( + json_schema_extra={ + "description": "An identifier for this style. Implementation of 'identifier'", + } + ), + ] + title: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "A title for this style", + } + ), + ] = None + description: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Brief narrative description of this style", + } + ), + ] = None + keywords: Annotated[ + Optional[List[str]], + Field( + json_schema_extra={ + "description": "keywords about this style", + } + ), + ] = None + links: Annotated[ + Optional[List[Link]], + Field( + min_length=1, + json_schema_extra={ + "description": "Links to style related resources. Possible link 'rel' values are: 'style' for a URL pointing to the style description, 'styleSpec' for a URL pointing to the specification or standard used to define the style.", + }, + ), + ] = None + + +class GeospatialData(BaseModel): + """Geospatial model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/geospatialData.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + title: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Title of this tile matrix set, normally used for display to a human", + } + ), + ] = None + description: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Brief narrative description of this tile matrix set, normally available for display to a human", + } + ), + ] = None + keywords: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Unordered list of one or more commonly used or formalized word(s) or phrase(s) used to describe this layer", + } + ), + ] = None + id: Annotated[ + str, + Field( + json_schema_extra={ + "description": "Unique identifier of the Layer. Implementation of 'identifier'", + } + ), + ] + dataType: Annotated[ + Literal["map", "vector", "coverage"], + Field( + json_schema_extra={ + "description": "Type of data represented in the tileset", + } + ), + ] + geometryDimension: Annotated[ + Optional[int], + Field( # type: ignore + ge=0, + le=3, + json_schema_extra={ + "description": "The geometry dimension of the features shown in this layer (0: points, 1: curves, 2: surfaces, 3: solids), unspecified: mixed or unknown", + }, + ), + ] = None + featureType: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Feature type identifier. Only applicable to layers of datatype 'geometries'", + } + ), + ] = None + attribution: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Short reference to recognize the author or provider", + } + ), + ] = None + license: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "License applicable to the tiles", + } + ), + ] = None + pointOfContact: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Useful information to contact the authors or custodians for the layer (e.g. e-mail address, a physical address, phone numbers, etc)", + } + ), + ] = None + publisher: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Organization or individual responsible for making the layer available", + } + ), + ] = None + theme: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Category where the layer can be grouped", + } + ), + ] = None + crs: Annotated[Optional[CRSType], Field(json_schema_extra={"title": "CRS"})] = None + epoch: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Epoch of the Coordinate Reference System (CRS)", + } + ), + ] = None + minScaleDenominator: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Minimum scale denominator for usage of the layer", + } + ), + ] = None + maxScaleDenominator: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Maximum scale denominator for usage of the layer", + } + ), + ] = None + minCellSize: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Minimum cell size for usage of the layer", + } + ), + ] = None + maxCellSize: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Maximum cell size for usage of the layer", + } + ), + ] = None + maxTileMatrix: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "TileMatrix identifier associated with the minScaleDenominator", + } + ), + ] = None + minTileMatrix: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "TileMatrix identifier associated with the maxScaleDenominator", + } + ), + ] = None + boundingBox: Optional[BoundingBox] = None + created: Optional[TimeStamp] = None + updated: Optional[TimeStamp] = None + style: Optional[Style] = None + geoDataClasses: Annotated[ + Optional[List[str]], + Field( + json_schema_extra={ + "description": "URI identifying a class of data contained in this layer (useful to determine compatibility with styles or processes)", + } + ), + ] = None + propertiesSchema: Optional[PropertiesSchema] = None + links: Annotated[ + Optional[List[Link]], + Field( + min_length=1, + json_schema_extra={ + "description": "Links related to this layer. Possible link 'rel' values are: 'geodata' for a URL pointing to the collection of geospatial data.", + }, + ), + ] = None + + +class TilePoint(BaseModel): + """TilePoint model. + + Ref: https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/tilePoint.yaml + + Code generated using https://github.com/koxudaxi/datamodel-code-generator/ + """ + + coordinates: Annotated[List[float], Field(max_length=2, min_length=2)] + crs: Annotated[Optional[CRSType], Field(json_schema_extra={"title": "CRS"})] + tileMatrix: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "TileMatrix identifier associated with the scaleDenominator", + } + ), + ] = None + scaleDenominator: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Scale denominator of the tile matrix selected", + } + ), + ] = None + cellSize: Annotated[ + Optional[float], + Field( + json_schema_extra={ + "description": "Cell size of the tile matrix selected", + } + ), + ] = None + + +class TileMatrixLimits(BaseModel): + """ + The limits for an individual tile matrix of a TileSet's TileMatrixSet, as defined in the OGC 2D TileMatrixSet and TileSet Metadata Standard + + Based on https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/tileMatrixLimits.yaml + """ + + tileMatrix: str + minTileRow: Annotated[int, Field(ge=0)] + maxTileRow: Annotated[int, Field(ge=0)] + minTileCol: Annotated[int, Field(ge=0)] + maxTileCol: Annotated[int, Field(ge=0)] + + +class TileSet(BaseModel): + """ + TileSet model. + + Based on https://github.com/opengeospatial/ogcapi-tiles/blob/master/openapi/schemas/tms/tileSet.yaml + """ + + title: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "A title for this tileset", + } + ), + ] = None + description: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Brief narrative description of this tile set", + } + ), + ] = None + dataType: Annotated[ + Literal["map", "vector", "coverage"], + Field( + json_schema_extra={ + "description": "Type of data represented in the tileset", + } + ), + ] + crs: Annotated[CRSType, Field(json_schema_extra={"title": "CRS"})] + tileMatrixSetURI: Annotated[ + Optional[AnyUrl], + Field( + json_schema_extra={ + "description": "Reference to a Tile Matrix Set on an official source for Tile Matrix Sets", + } + ), + ] = None + links: Annotated[ + List[Link], + Field( + json_schema_extra={ + "description": "Links to related resources", + } + ), + ] + tileMatrixSetLimits: Annotated[ + Optional[List[TileMatrixLimits]], + Field( + json_schema_extra={ + "description": "Limits for the TileRow and TileCol values for each TileMatrix in the tileMatrixSet. If missing, there are no limits other that the ones imposed by the TileMatrixSet. If present the TileMatrices listed are limited and the rest not available at all", + } + ), + ] = None + epoch: Annotated[ + Optional[Union[float, int]], + Field( + json_schema_extra={ + "description": "Epoch of the Coordinate Reference System (CRS)", + } + ), + ] = None + layers: Annotated[ + Optional[List[GeospatialData]], + Field(min_length=1), + ] = None + boundingBox: Optional[BoundingBox] = None + centerPoint: Optional[TilePoint] = None + style: Optional[Style] = None + attribution: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Short reference to recognize the author or provider", + } + ), + ] = None + license: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "License applicable to the tiles", + } + ), + ] = None + accessConstraints: Annotated[ + Optional[AccessConstraints], + Field( + json_schema_extra={ + "description": "Restrictions on the availability of the Tile Set that the user needs to be aware of before using or redistributing the Tile Set", + } + ), + ] = "unclassified" + keywords: Annotated[ + Optional[List[str]], + Field( + json_schema_extra={ + "description": "keywords about this tileset", + } + ), + ] = None + version: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Version of the Tile Set. Changes if the data behind the tiles has been changed", + } + ), + ] = None + created: Optional[TimeStamp] = None + updated: Optional[TimeStamp] = None + pointOfContact: Annotated[ + Optional[str], + Field( + json_schema_extra={ + "description": "Useful information to contact the authors or custodians for the Tile Set", + } + ), + ] = None + mediaTypes: Annotated[ + Optional[List[str]], + Field( + json_schema_extra={ + "description": "Media types available for the tiles", + } + ), + ] = None + + +class TileSetList(BaseModel): + """ + TileSetList model. + + Based on https://docs.ogc.org/is/20-057/20-057.html#toc34 + """ + + tilesets: List[TileSet] + + +class Conformance(BaseModel): + """Conformance model. + + Ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/schemas/confClasses.yaml + + """ + + conformsTo: List[str] + + +class Landing(BaseModel): + """Landing page model. + + Ref: http://schemas.opengis.net/ogcapi/features/part1/1.0/openapi/schemas/landingPage.yaml + + """ + + title: Optional[str] = None + description: Optional[str] = None + links: List[Link] diff --git a/src/titiler/core/titiler/core/models/mapbox.py b/src/titiler/core/titiler/core/models/mapbox.py index 755ce6901..a52962e1b 100644 --- a/src/titiler/core/titiler/core/models/mapbox.py +++ b/src/titiler/core/titiler/core/models/mapbox.py @@ -1,16 +1,8 @@ """Common response models.""" -from enum import Enum -from typing import List, Optional, Tuple +from typing import List, Literal, Optional, Tuple -from pydantic import BaseModel, Field, root_validator - - -class SchemeEnum(str, Enum): - """TileJSON scheme choice.""" - - xyz = "xyz" - tms = "tms" +from pydantic import BaseModel, Field, model_validator class TileJSON(BaseModel): @@ -22,34 +14,29 @@ class TileJSON(BaseModel): """ tilejson: str = "2.2.0" - name: Optional[str] - description: Optional[str] + name: Optional[str] = None + description: Optional[str] = None version: str = "1.0.0" - attribution: Optional[str] - template: Optional[str] - legend: Optional[str] - scheme: SchemeEnum = SchemeEnum.xyz + attribution: Optional[str] = None + template: Optional[str] = None + legend: Optional[str] = None + scheme: Literal["xyz", "tms"] = "xyz" tiles: List[str] - grids: Optional[List[str]] - data: Optional[List[str]] + grids: Optional[List[str]] = None + data: Optional[List[str]] = None minzoom: int = Field(0, ge=0, le=30) maxzoom: int = Field(30, ge=0, le=30) bounds: List[float] = [-180, -90, 180, 90] - center: Optional[Tuple[float, float, int]] + center: Optional[Tuple[float, float, int]] = None - @root_validator - def compute_center(cls, values): + @model_validator(mode="after") + def compute_center(self): """Compute center if it does not exist.""" - bounds = values["bounds"] - if not values.get("center"): - values["center"] = ( + bounds = self.bounds + if not self.center: + self.center = ( (bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2, - values["minzoom"], + self.minzoom, ) - return values - - class Config: - """TileJSON model configuration.""" - - use_enum_values = True + return self diff --git a/src/titiler/core/titiler/core/models/responses.py b/src/titiler/core/titiler/core/models/responses.py index 7932c02e8..27ada7b11 100644 --- a/src/titiler/core/titiler/core/models/responses.py +++ b/src/titiler/core/titiler/core/models/responses.py @@ -1,12 +1,14 @@ """TiTiler response models.""" -from typing import Dict, List, Union +from typing import Dict, List, Optional, Union from geojson_pydantic.features import Feature, FeatureCollection -from geojson_pydantic.geometries import Geometry, Polygon +from geojson_pydantic.geometries import Geometry, MultiPolygon, Polygon from pydantic import BaseModel from rio_tiler.models import BandStatistics, Info +from titiler.core.models.OGC import Link + class Point(BaseModel): """ @@ -17,11 +19,11 @@ class Point(BaseModel): """ coordinates: List[float] - values: List[float] + values: List[Optional[float]] band_names: List[str] -InfoGeoJSON = Feature[Polygon, Info] +InfoGeoJSON = Feature[Union[Polygon, MultiPolygon], Info] Statistics = Dict[str, BandStatistics] @@ -30,23 +32,24 @@ class StatisticsInGeoJSON(BaseModel): statistics: Statistics - class Config: - """Config for model.""" - - extra = "allow" + model_config = {"extra": "allow"} StatisticsGeoJSON = Union[ - FeatureCollection[Geometry, StatisticsInGeoJSON], + FeatureCollection[Feature[Geometry, StatisticsInGeoJSON]], Feature[Geometry, StatisticsInGeoJSON], ] # MultiBase Models MultiBaseInfo = Dict[str, Info] -MultiBaseInfoGeoJSON = Feature[Polygon, MultiBaseInfo] +MultiBaseInfoGeoJSON = Feature[Union[Polygon, MultiPolygon], MultiBaseInfo] MultiBaseStatistics = Dict[str, Statistics] -MultiBaseStatisticsGeoJSON = Union[ - FeatureCollection[Geometry, StatisticsInGeoJSON], - Feature[Geometry, StatisticsInGeoJSON], -] +MultiBaseStatisticsGeoJSON = StatisticsGeoJSON + + +class ColorMapsList(BaseModel): + """Model for colormap list.""" + + colorMaps: List[str] + links: List[Link] diff --git a/src/titiler/core/titiler/core/py.typed b/src/titiler/core/titiler/core/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/src/titiler/core/titiler/core/resources/enums.py b/src/titiler/core/titiler/core/resources/enums.py index 1f0fbfa32..52be51fdb 100644 --- a/src/titiler/core/titiler/core/resources/enums.py +++ b/src/titiler/core/titiler/core/resources/enums.py @@ -23,7 +23,14 @@ class MediaType(str, Enum): html = "text/html" text = "text/plain" pbf = "application/x-protobuf" - mvt = "application/x-protobuf" + mvt = "application/vnd.mapbox-vector-tile" + ndjson = "application/ndjson" + geojsonseq = "application/geo+json-seq" + schemajson = "application/schema+json" + csv = "text/csv" + openapi30_json = "application/vnd.oai.openapi+json;version=3.0" + openapi30_yaml = "application/vnd.oai.openapi;version=3.0" + gif = "image/gif" class ImageDriver(str, Enum): @@ -37,6 +44,7 @@ class ImageDriver(str, Enum): webp = "WEBP" jp2 = "JP2OpenJPEG" npy = "NPY" + gif = "GIF" class ImageType(str, Enum): diff --git a/src/titiler/core/titiler/core/resources/responses.py b/src/titiler/core/titiler/core/resources/responses.py index cd37a6a85..3a99d6be5 100644 --- a/src/titiler/core/titiler/core/resources/responses.py +++ b/src/titiler/core/titiler/core/resources/responses.py @@ -2,6 +2,7 @@ from typing import Any +import numpy import simplejson as json from starlette import responses @@ -12,6 +13,16 @@ class XMLResponse(responses.Response): media_type = "application/xml" +class NumpyEncoder(json.JSONEncoder): + """Custom JSON Encoder.""" + + def default(self, obj): + """Catch numpy types and convert them.""" + if isinstance(obj, (numpy.ndarray, numpy.generic)): + return obj.tolist() + return super().default(obj) + + class JSONResponse(responses.JSONResponse): """Custom JSON Response.""" @@ -27,6 +38,7 @@ def render(self, content: Any) -> bytes: indent=None, ignore_nan=True, separators=(",", ":"), + cls=NumpyEncoder, ).encode("utf-8") diff --git a/src/titiler/core/titiler/core/routing.py b/src/titiler/core/titiler/core/routing.py index 28b66d6ec..7c85991ce 100644 --- a/src/titiler/core/titiler/core/routing.py +++ b/src/titiler/core/titiler/core/routing.py @@ -1,6 +1,5 @@ """Custom routing classes.""" -import sys import warnings from typing import Callable, Dict, List, Optional, Type @@ -11,11 +10,7 @@ from starlette.requests import Request from starlette.responses import Response from starlette.routing import BaseRoute, Match - -if sys.version_info >= (3, 8): - from typing import TypedDict # pylint: disable=no-name-in-module -else: - from typing_extensions import TypedDict +from typing_extensions import TypedDict def apiroute_factory(env: Optional[Dict] = None) -> Type[APIRoute]: @@ -34,6 +29,7 @@ def apiroute_factory(env: Optional[Dict] = None) -> Type[APIRoute]: "'apiroute_factory' has been deprecated and will be removed" "in titiler 0.1.0. Please see `environment_dependency` option in endpoint factories.", DeprecationWarning, + stacklevel=1, ) class EnvAPIRoute(APIRoute): @@ -87,7 +83,8 @@ def add_route_dependencies( route.dependant.dependencies.insert( # type: ignore 0, get_parameterless_sub_dependant( - depends=depends, path=route.path_format # type: ignore + depends=depends, + path=route.path_format, # type: ignore ), ) diff --git a/src/titiler/core/titiler/core/telemetry.py b/src/titiler/core/titiler/core/telemetry.py new file mode 100644 index 000000000..a3463bfa6 --- /dev/null +++ b/src/titiler/core/titiler/core/telemetry.py @@ -0,0 +1,141 @@ +"""OpenTelemetry instrumentation for titiler.core.""" + +import functools +import inspect +from contextlib import contextmanager +from typing import Any, Callable, Dict, Iterator, Optional, TypeVar + +from typing_extensions import ParamSpec + +from titiler.core import __version__ + +try: + from opentelemetry import trace + from opentelemetry.trace import Span, Status, StatusCode + + tracer = trace.get_tracer("titiler.core", __version__) +except ImportError: + trace = None + Span = None + Status = None + StatusCode = None + tracer = None + +P = ParamSpec("P") +R = TypeVar("R") + + +def add_span_attributes(attributes: Dict[str, Any]) -> None: + """Adds attributes to the current active span.""" + if not tracer: + return + span = trace.get_current_span() + if span and span.is_recording(): + span.set_attributes(attributes) + + +def flatten_dict(d: dict, parent_key: str = "", sep: str = ".") -> dict: + """Flattens a nested dictionary for adding span attributes.""" + items = {} + for k, v in d.items(): + new_key = parent_key + sep + k if parent_key else k + if isinstance(v, dict): + items.update(flatten_dict(v, new_key, sep=sep)) + else: + if not isinstance(v, (str, bool, int, float)): + v = str(v) + items[new_key] = v + return items + + +class SpanWrapper: + """A wrapper class to safely handle an optional OpenTelemetry Span.""" + + def __init__(self, span: Optional[Span]): + """Set the span""" + self._span = span + + def set_attributes(self, attributes: Dict[str, Any]) -> None: + """Safely set attributes on the wrapped span if it exists.""" + if self._span: + self._span.set_attributes(attributes) + + def record_exception(self, exception: Exception) -> None: + """Safely record an exception on the wrapped span if it exists.""" + if self._span: + self._span.record_exception(exception) + + +@contextmanager +def operation_tracer( + operation_name: str, + attributes: Optional[Dict[str, Any]] = None, +) -> Iterator[SpanWrapper]: + """Context manager for creating granular child spans.""" + if not tracer: + yield SpanWrapper(None) + return + + with tracer.start_as_current_span(operation_name) as span: + wrapped_span = SpanWrapper(span) + if attributes: + wrapped_span.set_attributes(attributes) + try: + yield wrapped_span + span.set_status(Status(StatusCode.OK)) + except Exception as e: + span.set_status(Status(StatusCode.ERROR, str(e))) + raise + + +def _get_span_name(op_name: str, factory_instance: Any) -> str: + """Determine the span name using the factory class name.""" + if not factory_instance: + return op_name + class_name = factory_instance.__class__.__name__ + return f"{class_name}.{op_name}" + + +def factory_trace( + _func: Optional[Callable[P, Any]] = None, + *, + factory_instance: Optional[Any] = None, +) -> Any: + """A decorator for Factory methods that automatically handles tracing for factory methods""" + + def decorator(func: Callable[P, Any]) -> Callable[P, Any]: + if not tracer: + return func + + op_name = func.__name__ + + attributes = {} + if factory_instance: + if hasattr(factory_instance, "reader"): + attributes["reader"] = str(factory_instance.reader) + if hasattr(factory_instance, "backend"): + attributes["backend"] = str(factory_instance.backend) + + if inspect.iscoroutinefunction(func): + + @functools.wraps(func) + async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: + span_name = _get_span_name(op_name, factory_instance) + with operation_tracer(span_name, attributes=attributes): + return await func(*args, **kwargs) + + return async_wrapper + else: + + @functools.wraps(func) + def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: + span_name = _get_span_name(op_name, factory_instance) + with operation_tracer(span_name, attributes=attributes): + return func(*args, **kwargs) + + return sync_wrapper + + return decorator if _func is None else decorator(_func) + + +factory_trace.decorator_enabled = bool(tracer) # type: ignore [attr-defined] diff --git a/src/titiler/core/titiler/core/templates/map.html b/src/titiler/core/titiler/core/templates/map.html index 700b2dae8..1d36688e1 100644 --- a/src/titiler/core/titiler/core/templates/map.html +++ b/src/titiler/core/titiler/core/templates/map.html @@ -1,124 +1,290 @@ - - - - - TiTiler Map Viewer - - - - - - - - - -
+ + + + + TiTiler Map Viewer + + + + + + + + +
- - + + diff --git a/src/titiler/core/titiler/core/templates/wmts.xml b/src/titiler/core/titiler/core/templates/wmts.xml index a42f54095..b13340fc4 100644 --- a/src/titiler/core/titiler/core/templates/wmts.xml +++ b/src/titiler/core/titiler/core/templates/wmts.xml @@ -1,6 +1,6 @@ - "{{ title }}" + {{ title }} OGC WMTS 1.0.0 @@ -8,7 +8,7 @@ - + RESTful @@ -21,7 +21,7 @@ - + RESTful @@ -33,30 +33,32 @@ + {% for layer in layers -%} - {{ title }} - {{ layer_name }} - {{ title }} - - {{ bounds[0] }} {{ bounds[1] }} - {{ bounds[2] }} {{ bounds[3] }} - + {{ layer.title }} + {{ layer.name }} + {{ layer.name }} + + {{ layer.bounds[0] }} {{ layer.bounds[1] }} + {{ layer.bounds[2] }} {{ layer.bounds[3] }} + {{ media_type }} - {{ tms.identifier }} + {{ tileMatrixSetId }} - + + {%- endfor %} - {{ tms.identifier }} - {{ tms.crs.srs }} - {% for item in tileMatrix %} + {{ tileMatrixSetId }} + {{ supported_crs }} + {% for item in tileMatrix -%} {{ item | safe }} - {% endfor %} + {%- endfor %} - + diff --git a/src/titiler/core/titiler/core/templating/__init__.py b/src/titiler/core/titiler/core/templating/__init__.py new file mode 100644 index 000000000..e9f8d8f1d --- /dev/null +++ b/src/titiler/core/titiler/core/templating/__init__.py @@ -0,0 +1,77 @@ +"""titiler.core HTML templating.""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +import jinja2 +from starlette.templating import Jinja2Templates + +if TYPE_CHECKING: + from typing import Any + + from starlette.requests import Request + from starlette.templating import _TemplateResponse + + +jinja2_env = jinja2.Environment( + loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "html")]) +) +DEFAULT_TEMPLATES = Jinja2Templates(env=jinja2_env) + + +def create_html_response( + request: Request, + data: Any, + template_name: str, + templates: Jinja2Templates | None = None, + title: str | None = None, + router_prefix: str | None = None, + **kwargs: Any, +) -> _TemplateResponse: + """Create Template response.""" + + templates = templates or DEFAULT_TEMPLATES + + urlpath = request.url.path + if root_path := request.scope.get("root_path"): + urlpath = re.sub(r"^" + root_path, "", urlpath) + + if router_prefix: + urlpath = re.sub(r"^" + router_prefix, "", urlpath) + + crumbs = [] + baseurl = str(request.base_url).rstrip("/") + + if router_prefix: + baseurl += router_prefix + + crumbpath = str(baseurl) + if urlpath == "/": + urlpath = "" + + for crumb in urlpath.split("/"): + crumbpath = crumbpath.rstrip("/") + part = crumb + if part is None or part == "": + part = "Home" + crumbpath += f"/{crumb}" + crumbs.append({"url": crumbpath.rstrip("/"), "part": part.capitalize()}) + + return templates.TemplateResponse( + request, + name=f"{template_name}.html", + context={ + "response": data, + "template": { + "api_root": baseurl, + "params": request.query_params, + "title": title or template_name, + }, + "crumbs": crumbs, + "url": baseurl + urlpath, + "params": str(request.url.query), + **kwargs, + }, + ) diff --git a/src/titiler/core/titiler/core/templating/html/base.html b/src/titiler/core/titiler/core/templating/html/base.html new file mode 100644 index 000000000..49764d1b2 --- /dev/null +++ b/src/titiler/core/titiler/core/templating/html/base.html @@ -0,0 +1,48 @@ + + + + {{ template.title }} + + + + + + + + + + +
+
+ {% block content %}{% endblock %} + {% include "debug.html" %} +
+
+ + diff --git a/src/titiler/core/titiler/core/templating/html/conformance.html b/src/titiler/core/titiler/core/templating/html/conformance.html new file mode 100644 index 000000000..0471b3683 --- /dev/null +++ b/src/titiler/core/titiler/core/templating/html/conformance.html @@ -0,0 +1,32 @@ +{% include "header.html" %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + +

{{ template.title }}

+ +

This API implements the conformance classes from standards and community specifications that are listed below.

+ +

Links

+
    +{% for url in response.conformsTo %} +
  • {{ url }}
  • +{% endfor %} +
+ +{% include "footer.html" %} diff --git a/src/titiler/core/titiler/core/templating/html/debug.html b/src/titiler/core/titiler/core/templating/html/debug.html new file mode 100644 index 000000000..e69de29bb diff --git a/src/titiler/core/titiler/core/templating/html/footer.html b/src/titiler/core/titiler/core/templating/html/footer.html new file mode 100644 index 000000000..0519bcd8d --- /dev/null +++ b/src/titiler/core/titiler/core/templating/html/footer.html @@ -0,0 +1,16 @@ + {% include "debug.html" %} + + + +
+
+ Created by +
+ + Development Seed + +
+
+ + diff --git a/src/titiler/core/titiler/core/templating/html/header.html b/src/titiler/core/titiler/core/templating/html/header.html new file mode 100644 index 000000000..21f75e067 --- /dev/null +++ b/src/titiler/core/titiler/core/templating/html/header.html @@ -0,0 +1,42 @@ + + + + {{ template.title }} + + + + + + + + + + +
+
diff --git a/src/titiler/core/titiler/core/templating/html/icons/clock.html b/src/titiler/core/titiler/core/templating/html/icons/clock.html new file mode 100644 index 000000000..139df0679 --- /dev/null +++ b/src/titiler/core/titiler/core/templating/html/icons/clock.html @@ -0,0 +1 @@ + diff --git a/src/titiler/core/titiler/core/templating/html/icons/license.html b/src/titiler/core/titiler/core/templating/html/icons/license.html new file mode 100644 index 000000000..624f77ec2 --- /dev/null +++ b/src/titiler/core/titiler/core/templating/html/icons/license.html @@ -0,0 +1 @@ + diff --git a/src/titiler/core/titiler/core/templating/html/icons/tag.html b/src/titiler/core/titiler/core/templating/html/icons/tag.html new file mode 100644 index 000000000..de48df803 --- /dev/null +++ b/src/titiler/core/titiler/core/templating/html/icons/tag.html @@ -0,0 +1 @@ + diff --git a/src/titiler/core/titiler/core/templating/html/landing.html b/src/titiler/core/titiler/core/templating/html/landing.html new file mode 100644 index 000000000..715d91d5b --- /dev/null +++ b/src/titiler/core/titiler/core/templating/html/landing.html @@ -0,0 +1,42 @@ +{% include "header.html" %} +{% if params %} + {% set urlq = url + '?' + params + '&' %} + {% else %} + {% set urlq = url + '?' %} +{% endif %} + + + +

{{ response.title }}

+

+ {{ response.description }} +

+ +
+  ______   __     ______   __     __         ______     ______
+/\__  _\ /\ \   /\__  _\ /\ \   /\ \       /\  ___\   /\  == \
+\/_/\ \/ \ \ \  \/_/\ \/ \ \ \  \ \ \____  \ \  __\   \ \  __<
+    \ \_\  \ \_\    \ \_\  \ \_\  \ \_____\  \ \_____\  \ \_\ \_\
+    \/_/   \/_/     \/_/   \/_/   \/_____/   \/_____/   \/_/ /_/
+
+  
+ +

Links

+ + +{% include "footer.html" %} diff --git a/src/titiler/core/titiler/core/utils.py b/src/titiler/core/titiler/core/utils.py new file mode 100644 index 000000000..98fbfd553 --- /dev/null +++ b/src/titiler/core/titiler/core/utils.py @@ -0,0 +1,335 @@ +"""titiler.core utilities.""" + +import warnings +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, TypeVar, Union +from urllib.parse import urlencode + +import numpy +from fastapi import FastAPI +from fastapi.datastructures import QueryParams +from fastapi.dependencies.utils import get_dependant, request_params_to_args +from geojson_pydantic.geometries import MultiPolygon, Polygon +from rasterio.dtypes import dtype_ranges +from rio_tiler.colormap import apply_cmap +from rio_tiler.errors import InvalidDatatypeWarning +from rio_tiler.models import ImageData +from rio_tiler.types import BBox, ColorMapType, IntervalTuple +from rio_tiler.utils import linear_rescale, render +from starlette.requests import Request +from starlette.responses import Response +from starlette.routing import Route, request_response + +from titiler.core.resources.enums import ImageType, MediaType + + +def rescale_array( + array: numpy.ndarray, + mask: numpy.ndarray, + in_range: Sequence[IntervalTuple], + out_range: Sequence[IntervalTuple] = ((0, 255),), + out_dtype: Union[str, numpy.number] = "uint8", +) -> numpy.ndarray: + """Rescale data array""" + if len(array.shape) < 3: + array = numpy.expand_dims(array, axis=0) + + nbands = array.shape[0] + if len(in_range) != nbands: + in_range = ((in_range[0]),) * nbands + + if len(out_range) != nbands: + out_range = ((out_range[0]),) * nbands + + for bdx in range(nbands): + array[bdx] = numpy.where( + mask[bdx], + linear_rescale( + array[bdx], in_range=in_range[bdx], out_range=out_range[bdx] + ), + 0, + ) + + return array.astype(out_dtype) + + +def render_image( # noqa: C901 + image: ImageData, + colormap: Optional[ColorMapType] = None, + output_format: Optional[ImageType] = None, + add_mask: bool = True, + rescale: Optional[Sequence[IntervalTuple]] = None, + color_formula: Optional[str] = None, + **kwargs: Any, +) -> Tuple[bytes, str]: + """convert image data to file. + + This is adapted from https://github.com/cogeotiff/rio-tiler/blob/066878704f841a332a53027b74f7e0a97f10f4b2/rio_tiler/models.py#L698-L764 + """ + if rescale: + image.rescale(rescale) + + if color_formula: + image.apply_color_formula(color_formula) + + data, mask = image.data.copy(), image.mask.copy() + datatype_range = image.dataset_statistics or (dtype_ranges[str(data.dtype)],) + + if colormap: + data, alpha_from_cmap = apply_cmap(data, colormap) + # Combine both Mask from dataset and Alpha band from Colormap + mask = numpy.bitwise_and(alpha_from_cmap, mask) + datatype_range = (dtype_ranges[str(data.dtype)],) + + # If output_format is not set, we choose between JPEG and PNG + if not output_format: + output_format = ImageType.jpeg if mask.all() else ImageType.png + + # format-specific valid dtypes + format_dtypes = { + ImageType.png: ["uint8", "uint16"], + ImageType.jpeg: ["uint8"], + ImageType.jpg: ["uint8"], + ImageType.webp: ["uint8"], + ImageType.jp2: ["uint8", "int16", "uint16"], + } + + valid_dtypes = format_dtypes.get(output_format, []) + if valid_dtypes and data.dtype not in valid_dtypes: + warnings.warn( + f"Invalid type: `{data.dtype}` for the `{output_format}` driver. " + "Data will be rescaled using min/max type bounds or dataset_statistics.", + InvalidDatatypeWarning, + stacklevel=1, + ) + data = rescale_array(data, mask, in_range=datatype_range) + + creation_options = {**kwargs, **output_format.profile} + if output_format == ImageType.tif: + if "transform" not in creation_options: + creation_options.update({"transform": image.transform}) + if "crs" not in creation_options and image.crs: + creation_options.update({"crs": image.crs}) + + if not add_mask: + mask = None + + return ( + render( + data, + mask, + img_format=output_format.driver, + **creation_options, + ), + output_format.mediatype, + ) + + +def bounds_to_geometry(bounds: BBox) -> Union[Polygon, MultiPolygon]: + """Convert bounds to geometry. + + Note: if bounds are crossing the dateline separation line, a MultiPolygon geometry will be returned. + + """ + if bounds[0] > bounds[2]: + pl = Polygon.from_bounds(-180, bounds[1], bounds[2], bounds[3]) + pr = Polygon.from_bounds(bounds[0], bounds[1], 180, bounds[3]) + return MultiPolygon( + type="MultiPolygon", + coordinates=[pl.coordinates, pr.coordinates], + ) + return Polygon.from_bounds(*bounds) + + +T = TypeVar("T") + +ValidParams = Dict[str, Any] +Errors = List[Any] + + +def get_dependency_query_params( + dependency: Callable, + params: Union[QueryParams, Dict], +) -> Tuple[ValidParams, Errors]: + """Check QueryParams for Query dependency. + + 1. `get_dependant` is used to get the query-parameters required by the `callable` + 2. we use `request_params_to_args` to construct arguments needed to call the `callable` + 3. we call the `callable` and catch any errors + + Important: We assume the `callable` in not a co-routine. + """ + dep = get_dependant(path="", call=dependency) + + qp = ( + QueryParams(urlencode(params, doseq=True)) + if isinstance(params, Dict) + else params + ) + return request_params_to_args(dep.query_params, qp) + + +def deserialize_query_params( + dependency: Callable[..., T], params: Union[QueryParams, Dict] +) -> Tuple[T, Errors]: + """Deserialize QueryParams for given dependency. + + Parse params as query params and deserialize with dependency. + + Important: We assume the `callable` in not a co-routine. + """ + values, errors = get_dependency_query_params(dependency, params) + return dependency(**values), errors + + +def extract_query_params( + dependencies: List[Callable], + params: Union[QueryParams, Dict], +) -> Tuple[ValidParams, Errors]: + """Extract query params given list of dependencies.""" + values = {} + errors = [] + for dep in dependencies: + query_params, dep_errors = get_dependency_query_params(dep, params) + if query_params: + values.update(query_params) + errors += dep_errors + return values, errors + + +def check_query_params( + dependencies: List[Callable], params: Union[QueryParams, Dict] +) -> bool: + """Check QueryParams for Query dependency. + + 1. `get_dependant` is used to get the query-parameters required by the `callable` + 2. we use `request_params_to_args` to construct arguments needed to call the `callable` + 3. we call the `callable` and catch any errors + + Important: We assume the `callable` in not a co-routine + + """ + qp = ( + QueryParams(urlencode(params, doseq=True)) + if isinstance(params, Dict) + else params + ) + + for dependency in dependencies: + try: + dep = get_dependant(path="", call=dependency) + if dep.query_params: + # call the dependency with the query-parameters values + query_values, errors = request_params_to_args(dep.query_params, qp) + if errors: + return False + + _ = dependency(**query_values) + + except Exception: + return False + + return True + + +def accept_media_type(accept: str, mediatypes: List[MediaType]) -> Optional[MediaType]: + """Return MediaType based on accept header and available mediatype. + + Links: + - https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept + + """ + accept_values = {} + for m in accept.replace(" ", "").split(","): + values = m.split(";") + if len(values) == 1: + name = values[0] + quality = 1.0 + else: + name = values[0] + groups = dict([param.split("=") for param in values[1:]]) # type: ignore + try: + q = groups.get("q") + quality = float(q) if q else 1.0 + except ValueError: + quality = 0 + + # if quality is 0 we ignore encoding + if quality: + accept_values[name] = quality + + # Create Preference matrix + media_preference = { + v: [n for (n, q) in accept_values.items() if q == v] + for v in sorted(set(accept_values.values()), reverse=True) + } + + # Loop through available compression and encoding preference + for _, pref in media_preference.items(): + for media in mediatypes: + if media.value in pref: + return media + + # If no specified encoding is supported but "*" is accepted, + # take one of the available compressions. + if "*" in accept_values and mediatypes: + return mediatypes[0] + + return None + + +def update_openapi(app: FastAPI) -> FastAPI: + """Update OpenAPI response content-type. + + This function modifies the openapi route to comply with the STAC API spec's required + content-type response header. + + Copied from https://github.com/stac-utils/stac-fastapi/blob/main/stac_fastapi/api/stac_fastapi/api/openapi.py + + MIT License + + Copyright (c) 2020 Arturo AI + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + """ + # Find the route for the openapi_url in the app + openapi_route: Route = next( + route for route in app.router.routes if route.path == app.openapi_url + ) + # Store the old endpoint function so we can call it from the patched function + old_endpoint = openapi_route.endpoint + + # Create a patched endpoint function that modifies the content type of the response + async def patched_openapi_endpoint(req: Request) -> Response: + # Get the response from the old endpoint function + response = await old_endpoint(req) + # Update the content type header in place + response.headers["content-type"] = ( + "application/vnd.oai.openapi+json;version=3.0" + ) + # Return the updated response + return response + + # When a Route is accessed the `handle` function calls `self.app`. Which is + # the endpoint function wrapped with `request_response`. So we need to wrap + # our patched function and replace the existing app with it. + openapi_route.app = request_response(patched_openapi_endpoint) + + # return the patched app + return app diff --git a/src/titiler/extensions/LICENSE b/src/titiler/extensions/LICENSE new file mode 100644 index 000000000..eb4365951 --- /dev/null +++ b/src/titiler/extensions/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Development Seed + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/titiler/extensions/README.md b/src/titiler/extensions/README.md index 4ae1309ea..0e86f240c 100644 --- a/src/titiler/extensions/README.md +++ b/src/titiler/extensions/README.md @@ -5,14 +5,14 @@ Extent TiTiler Tiler Factories ## Installation ```bash -$ pip install -U pip +python -m pip install -U pip # From Pypi -$ pip install titiler.extensions +python -m pip install titiler.extensions # Or from sources -$ git clone https://github.com/developmentseed/titiler.git -$ cd titiler && pip install -e titiler/core -e titiler/extensions +git clone https://github.com/developmentseed/titiler.git +cd titiler && python -m pip install -e src/titiler/core -e src/titiler/extensions ``` ## Available extensions @@ -58,19 +58,16 @@ tiler = TilerFactory( app.include_router(tiler.router, prefix="/cog") ``` -See [titiler.application](../application) for a full example. - - ## Create your own ```python from dataclasses import dataclass, field from typing import Tuple, List, Optional - +import rasterio from starlette.responses import Response from fastapi import Depends, FastAPI, Query -from titiler.core.factory import BaseTilerFactory, FactoryExtension, TilerFactory -from titiler.core.dependencies import RescalingParams +from titiler.core.factory import TilerFactory, FactoryExtension +from titiler.core.dependencies import ImageRenderingParams from titiler.core.factory import TilerFactory from titiler.core.resources.enums import ImageType @@ -82,8 +79,8 @@ class thumbnailExtension(FactoryExtension): # Set some options max_size: int = field(default=128) - # Register method is mandatory and must take a BaseTilerFactory object as input - def register(self, factory: BaseTilerFactory): + # Register method is mandatory and must take a TilerFactory object as input + def register(self, factory: TilerFactory): """Register endpoint to the tiler factory.""" # register an endpoint to the factory's router @@ -103,47 +100,37 @@ class thumbnailExtension(FactoryExtension): def thumbnail( # we can reuse the factory dependency src_path: str = Depends(factory.path_dependency), + reader_params=Depends(factory.reader_dependency), layer_params=Depends(factory.layer_dependency), dataset_params=Depends(factory.dataset_dependency), post_process=Depends(factory.process_dependency), - rescale: Optional[List[Tuple[float, ...]]] = Depends(RescalingParams), - color_formula: Optional[str] = Query( - None, - title="Color Formula", - description="rio-color formula (info: https://github.com/mapbox/rio-color)", - ), colormap=Depends(factory.colormap_dependency), render_params=Depends(factory.render_dependency), - reader_params=Depends(factory.reader_dependency), env=Depends(factory.environment_dependency), ): with rasterio.Env(**env): - with self.reader(src_path, **reader_params) as src_dst: - im = src.preview( - max_size=self.max_size, - **layer_params, - **dataset_params, - ) + with factory.reader(src_path, **reader_params.as_dict()) as src: + image = src.preview( + max_size=self.max_size, + **layer_params.as_dict(), + **dataset_params.as_dict(), + ) if post_process: image = post_process(image) - if rescale: - image.rescale(rescale) - - if color_formula: - image.apply_color_formula(color_formula) - format = ImageType.jpeg if image.mask.all() else ImageType.png - content = image.render( - img_format=format.driver, - colormap=colormap or dst_colormap, - **format.profile, - **render_params, + if post_process: + image = post_process(image) + + content, media_type = factory.render_func( + image, + colormap=colormap, + **render_params.as_dict(), ) - return Response(content, media_type=format.mediatype) + return Response(content, media_type=media_type) # Use it app = FastAPI() diff --git a/src/titiler/extensions/pyproject.toml b/src/titiler/extensions/pyproject.toml index 3c3972416..0c4adb746 100644 --- a/src/titiler/extensions/pyproject.toml +++ b/src/titiler/extensions/pyproject.toml @@ -1,8 +1,8 @@ [project] -name = "titiler.extensions" +name = "titiler-extensions" description = "Extensions for TiTiler Factories." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" authors = [ {name = "Vincent Sarago", email = "vincent@developmentseed.com"}, ] @@ -21,15 +21,17 @@ classifiers = [ "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: GIS", ] dynamic = ["version"] dependencies = [ - "titiler.core==0.11.7" + "titiler.core==0.22.4" ] [project.optional-dependencies] @@ -38,13 +40,13 @@ test = [ "pytest-cov", "pytest-asyncio", "httpx", - "jsonschema>=3.0", + "pystac[validation]>=1.0.0,<2.0.0", ] cogeo = [ - "rio-cogeo>=3.1,<4.0", + "rio-cogeo>=5.0,<6.0", ] stac = [ - "rio-stac>=0.6,<0.7", + "rio-stac>=0.8,<0.9", ] [project.urls] @@ -55,8 +57,8 @@ Source = "https://github.com/developmentseed/titiler" Changelog = "https://developmentseed.org/titiler/release-notes/" [build-system] -requires = ["pdm-pep517"] -build-backend = "pdm.pep517.api" +requires = ["pdm-backend"] +build-backend = "pdm.backend" [tool.pdm.version] source = "file" diff --git a/src/titiler/extensions/tests/fixtures/render_item.json b/src/titiler/extensions/tests/fixtures/render_item.json new file mode 100644 index 000000000..5f0b93fa3 --- /dev/null +++ b/src/titiler/extensions/tests/fixtures/render_item.json @@ -0,0 +1,324 @@ +{ + "type": "Feature", + "stac_version": "1.0.0", + "stac_extensions": [ + "https://stac-extensions.github.io/eo/v1.0.0/schema.json", + "https://stac-extensions.github.io/projection/v1.0.0/schema.json", + "https://stac-extensions.github.io/view/v1.0.0/schema.json", + "https://stac-extensions.github.io/render/v2.0.0/schema.json", + "https://stac-extensions.github.io/virtual-assets/v1.0.0/schema.json", + "https://stac-extensions.github.io/web-map-links/v1.2.0/schema.json" + ], + "id": "LC08_L1TP_044033_20210305_20210312_01_T1", + "properties": { + "gsd": 30, + "platform": "LANDSAT_8", + "instruments": [ + "OLI", + "TIRS" + ], + "eo:cloud_cover": 7.41, + "proj:epsg": 32610, + "view:sun_azimuth": 149.10910644, + "view:sun_elevation": 40.48243563, + "view:off_nadir": 0.001, + "landsat:scene_id": "LC80440332021064LGN00", + "landsat:processing_level": "L1TP", + "landsat:collection_number": "01", + "landsat:collection_category": "T1", + "landsat:cloud_cover_land": 7.4, + "landsat:wrs_path": "44", + "landsat:wrs_row": "33", + "datetime": "2021-03-05T18:45:37.619485Z", + "created": "2021-03-16T01:40:56.703Z", + "updated": "2021-03-16T01:40:56.703Z", + "renders": { + "thumbnail": { + "title": "Thumbnail", + "assets": [ + "B4", + "B3", + "B2" + ], + "rescale": [ + [ + 0, + 150 + ] + ], + "colormap_name": "rainbow", + "resampling": "bilinear", + "bidx": [ + 1 + ], + "width": 1024, + "height": 1024, + "bands": [ + "B4", + "B3", + "B2" + ] + }, + "ndvi": { + "title": "Normalized Difference Vegetation Index", + "assets": [ + "ndvi" + ], + "resampling": "average", + "colormap_name": "ylgn", + "extra_param": "that titiler does not know" + } + } + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.49680286164214, + 39.958062660227306 + ], + [ + -120.31547276090922, + 39.578858170656 + ], + [ + -120.82135075676177, + 37.82701417652536 + ], + [ + -122.9993441554352, + 38.2150173967007 + ], + [ + -122.49680286164214, + 39.958062660227306 + ] + ] + ] + }, + "links": [ + { + "href": "https://maps.example.com/xyz/{z}/{x}/{y}.png", + "rel": "xyz", + "type": "image/png", + "title": "RGB composite visualized through a XYZ" + }, + { + "rel": "xyz", + "type": "image/png", + "title": "NDVI", + "href": "https://api.cogeo.xyz/stac/preview.png?url=https://raw.githubusercontent.com/stac-extensions/raster/main/examples/item-landsat8.json&expression=(B5–B4)/(B5+B4)&max_size=512&width=512&resampling_method=average&rescale=-1,1&color_map=ylgn&return_mask=true", + "render": "ndvi" + }, + { + "rel": "collection", + "href": "https://landsat-stac.s3.amazonaws.com/collections/landsat-8-l1.json", + "type": "application/json", + "title": "The full collection" + } + ], + "assets": { + "index": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/index.html", + "type": "application/html", + "title": "HTML Page" + }, + "ANG": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_ANG.txt", + "type": "text/plain", + "title": "ANG Metadata", + "roles": [ + "metadata" + ] + }, + "MTL": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_MTL.txt", + "type": "text/plain", + "title": "MTL Metadata", + "roles": [ + "metadata" + ] + }, + "BQA": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_BQA.TIF", + "type": "image/tiff; application=geotiff", + "title": "Quality Band", + "roles": [ + "quality" + ] + }, + "B1": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B1.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B1", + "common_name": "coastal", + "center_wavelength": 0.48, + "full_width_half_max": 0.02 + } + ] + }, + "B2": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B2.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B2", + "common_name": "blue", + "center_wavelength": 0.44, + "full_width_half_max": 0.06 + } + ] + }, + "B3": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B3.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B3", + "common_name": "green", + "center_wavelength": 0.56, + "full_width_half_max": 0.06 + } + ] + }, + "B4": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B4.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B4", + "common_name": "red", + "center_wavelength": 0.65, + "full_width_half_max": 0.04 + } + ] + }, + "B5": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B5.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B5", + "common_name": "nir", + "center_wavelength": 0.86, + "full_width_half_max": 0.03 + } + ] + }, + "B6": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B6.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B6", + "common_name": "swir16", + "center_wavelength": 1.6, + "full_width_half_max": 0.08 + } + ] + }, + "B7": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B7.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B7", + "common_name": "swir22", + "center_wavelength": 2.2, + "full_width_half_max": 0.2 + } + ] + }, + "B8": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B8.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B8", + "common_name": "pan", + "center_wavelength": 0.59, + "full_width_half_max": 0.18 + } + ], + "gsd": 15 + }, + "B9": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B9.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B9", + "common_name": "cirrus", + "center_wavelength": 1.37, + "full_width_half_max": 0.02 + } + ] + }, + "B10": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B10.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B10", + "common_name": "lwir11", + "center_wavelength": 10.9, + "full_width_half_max": 0.8 + } + ], + "gsd": 100 + }, + "B11": { + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1/LC08_L1TP_044033_20210305_20210312_01_T1_B11.TIF", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "eo:bands": [ + { + "name": "B11", + "common_name": "lwir12", + "center_wavelength": 12, + "full_width_half_max": 1 + } + ], + "gsd": 100 + }, + "ndvi": { + "roles": [ + "virtual", + "data", + "index" + ], + "type": "image/vnd.stac.geotiff; cloud-optimized=true", + "href": "https://landsat-pds.s3.us-west-2.amazonaws.com/c1/L8/044/033/LC08_L1TP_044033_20210305_20210312_01_T1#/assets/NDVI", + "vrt:hrefs": [ + { + "key": "B4", + "href": "#/assets/B4" + }, + { + "key": "B5", + "href": "#/assets/B5" + } + ], + "title": "Normalized Difference Vegetation Index", + "vrt:algorithm": "band_arithmetic", + "vrt:algorithm_opts": { + "expression": "(B05-B04)/(B05+B04)", + "rescale": [ + [ + -1, + 1 + ] + ] + } + } + }, + "bbox": [ + -123.00234, + 37.82405, + -120.31321, + 39.95894 + ], + "collection": "landsat-8-l1-c1" +} \ No newline at end of file diff --git a/src/titiler/extensions/tests/test_stac_render.py b/src/titiler/extensions/tests/test_stac_render.py new file mode 100644 index 000000000..7fa1ce5df --- /dev/null +++ b/src/titiler/extensions/tests/test_stac_render.py @@ -0,0 +1,80 @@ +"""Test STAC Render extension.""" + +import os +from urllib.parse import urlencode + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from rio_tiler.io import STACReader + +from titiler.core.factory import MultiBaseTilerFactory +from titiler.extensions.render import stacRenderExtension + +stac_item = os.path.join(os.path.dirname(__file__), "fixtures", "render_item.json") + + +def test_stacExtension(): + """Test stacExtension class.""" + + stac_tiler = MultiBaseTilerFactory(reader=STACReader) + + stac_tiler_plus_stac_render = MultiBaseTilerFactory( + reader=STACReader, extensions=[stacRenderExtension()] + ) + # Check that we added two routes (/renders & /renders/{render_id}) + assert ( + len(stac_tiler_plus_stac_render.router.routes) + == len(stac_tiler.router.routes) + 2 + ) + + app = FastAPI() + app.include_router(stac_tiler_plus_stac_render.router) + with TestClient(app) as client: + response = client.get("/renders", params={"url": stac_item}) + assert response.status_code == 200 + body = response.json() + assert body["renders"] + assert body["links"] + + self_link = body["links"][0] + assert self_link["href"] == response.url + assert self_link["rel"] == "self" + + assert "ndvi" in body["renders"] + assert "thumbnail" in body["renders"] + + expected_params = { + "assets": ["ndvi"], + "colormap_name": "ylgn", + "resampling": "average", + "title": "Normalized Difference Vegetation Index", + "extra_param": "that titiler does not know", + } + assert body["renders"]["ndvi"]["params"] == expected_params + + links = body["renders"]["ndvi"]["links"] + assert len(links) == 3 + + stac_item_param = urlencode({"url": stac_item}) + additional_params = "title=Normalized+Difference+Vegetation+Index&assets=ndvi&resampling=average&colormap_name=ylgn&extra_param=that+titiler+does+not+know" + hrefs = {link["href"] for link in links} + expected_hrefs = { + f"http://testserver/renders/ndvi?{stac_item_param}", + f"http://testserver/{{tileMatrixSetId}}/WMTSCapabilities.xml?{stac_item_param}&{additional_params}", + f"http://testserver/{{tileMatrixSetId}}/tilejson.json?{stac_item_param}&{additional_params}", + } + assert hrefs == expected_hrefs + + response = client.get("/renders/unknown", params={"url": stac_item}) + assert response.status_code == 404 + body = response.json() + assert body == {"detail": "Render not found"} + + response = client.get("/renders/ndvi", params={"url": stac_item}) + assert response.status_code == 200 + body = response.json() + assert body["params"] + assert body["links"] + hrefs = {link["href"] for link in links} + assert hrefs == expected_hrefs + assert body["params"] == expected_params diff --git a/src/titiler/extensions/tests/test_viewer.py b/src/titiler/extensions/tests/test_viewer.py index 2a56f119b..18a4cd929 100644 --- a/src/titiler/extensions/tests/test_viewer.py +++ b/src/titiler/extensions/tests/test_viewer.py @@ -25,7 +25,9 @@ def test_cogViewerExtension(): def test_stacViewerExtension(): """Test stacViewerExtension class.""" tiler = MultiBaseTilerFactory(reader=STACReader) - tiler_plus_viewer = MultiBaseTilerFactory(extensions=[stacViewerExtension()]) + tiler_plus_viewer = MultiBaseTilerFactory( + reader=STACReader, extensions=[stacViewerExtension()] + ) assert len(tiler_plus_viewer.router.routes) == len(tiler.router.routes) + 1 app = FastAPI() diff --git a/src/titiler/extensions/tests/test_wms.py b/src/titiler/extensions/tests/test_wms.py index 993817f1f..c7aa2f6de 100644 --- a/src/titiler/extensions/tests/test_wms.py +++ b/src/titiler/extensions/tests/test_wms.py @@ -386,3 +386,53 @@ def test_wmsExtension_GetMap(): -52.301598718454485, 74.66298001264106, ] + + +def test_wmsExtension_GetFeatureInfo(): + """Test wmsValidateExtension class for GetFeatureInfo request.""" + tiler_plus_wms = TilerFactory(extensions=[wmsExtension()]) + + app = FastAPI() + app.include_router(tiler_plus_wms.router) + + with TestClient(app) as client: + # Setup the basic GetFeatureInfo request + params = { + "VERSION": "1.3.0", + "REQUEST": "GetFeatureInfo", + "LAYERS": cog, + "QUERY_LAYERS": cog, + "BBOX": "500975.102,8182890.453,501830.647,8183959.884", + "CRS": "EPSG:32621", + "WIDTH": 334, + "HEIGHT": 333, + "INFO_FORMAT": "text/html", + "I": "0", + "J": "0", + } + + response = client.get("/wms", params=params) + + assert response.status_code == 200 + assert response.content == b"2800" + + params = { + "VERSION": "1.3.0", + "REQUEST": "GetFeatureInfo", + "LAYERS": cog, + "QUERY_LAYERS": cog, + "BBOX": "500975.102,8182890.453,501830.647,8183959.884", + "CRS": "EPSG:32621", + "WIDTH": 334, + "HEIGHT": 333, + "INFO_FORMAT": "text/html", + "I": "333", + "J": "332", + } + + response = client.get("/wms", params=params) + + assert response.status_code == 200 + assert response.content == b"3776" + + # Add additional assertions to check the text response diff --git a/src/titiler/extensions/titiler/extensions/__init__.py b/src/titiler/extensions/titiler/extensions/__init__.py index 18c28ff64..b821c4e58 100644 --- a/src/titiler/extensions/titiler/extensions/__init__.py +++ b/src/titiler/extensions/titiler/extensions/__init__.py @@ -1,8 +1,9 @@ """titiler.extensions""" -__version__ = "0.11.7" +__version__ = "0.22.4" from .cogeo import cogValidateExtension # noqa +from .render import stacRenderExtension # noqa from .stac import stacExtension # noqa from .viewer import cogViewerExtension, stacViewerExtension # noqa from .wms import wmsExtension # noqa diff --git a/src/titiler/extensions/titiler/extensions/cogeo.py b/src/titiler/extensions/titiler/extensions/cogeo.py index 5b0193060..da1f90d11 100644 --- a/src/titiler/extensions/titiler/extensions/cogeo.py +++ b/src/titiler/extensions/titiler/extensions/cogeo.py @@ -1,10 +1,11 @@ """rio-cogeo Extension.""" -from dataclasses import dataclass - +from attrs import define from fastapi import Depends, Query +from typing_extensions import Annotated -from titiler.core.factory import BaseTilerFactory, FactoryExtension +from titiler.core.factory import FactoryExtension, TilerFactory +from titiler.core.resources.responses import JSONResponse try: from rio_cogeo.cogeo import cog_info @@ -14,21 +15,29 @@ Info = None -@dataclass +@define class cogValidateExtension(FactoryExtension): """Add /validate endpoint to a COG TilerFactory.""" - def register(self, factory: BaseTilerFactory): + def register(self, factory: TilerFactory): """Register endpoint to the tiler factory.""" assert ( cog_info is not None - ), "'rio_cogeo' must be installed to use CogValidateExtension" - - @factory.router.get("/validate", response_model=Info) + ), "'rio-cogeo' must be installed to use CogValidateExtension" + + @factory.router.get( + "/validate", + response_model=Info, + response_class=JSONResponse, + operation_id=f"{factory.operation_prefix}validate", + ) def validate( - src_path: str = Depends(factory.path_dependency), - strict: bool = Query(False, description="Treat warnings as errors"), + src_path=Depends(factory.path_dependency), + strict: Annotated[ + bool, + Query(description="Treat warnings as errors"), + ] = False, ): """Validate a COG""" return cog_info(src_path, strict=strict) diff --git a/src/titiler/extensions/titiler/extensions/py.typed b/src/titiler/extensions/titiler/extensions/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/src/titiler/extensions/titiler/extensions/render.py b/src/titiler/extensions/titiler/extensions/render.py new file mode 100644 index 000000000..0435e6474 --- /dev/null +++ b/src/titiler/extensions/titiler/extensions/render.py @@ -0,0 +1,174 @@ +"""STAC Render Extension. + +Implements support for reading and applying Item level render extension. +See: https://github.com/stac-extensions/render +""" + +from typing import Dict, List, Optional +from urllib.parse import urlencode + +from attrs import define +from fastapi import Depends, HTTPException, Path, Request +from pydantic import BaseModel +from typing_extensions import Annotated + +from titiler.core.factory import FactoryExtension, MultiBaseTilerFactory +from titiler.core.models.OGC import Link +from titiler.core.utils import check_query_params + + +class RenderItem(BaseModel, extra="allow"): + """Render item for stac render extension.""" + + assets: List[str] + title: Optional[str] = None + rescale: Optional[List[Annotated[List[float], 2]]] = None + nodata: Optional[float] = None + colormap_name: Optional[str] = None + colormap: Optional[Dict] = None + color_formula: Optional[str] = None + resampling: Optional[str] = None + expression: Optional[str] = None + minmax_zoom: Optional[Annotated[List[int], 2]] = None + + +class RenderItemWithLinks(BaseModel): + """Same as RenderItem with url and params.""" + + params: RenderItem + links: List[Link] + + +class RenderItemList(BaseModel): + """List of Render Items with links.""" + + renders: Dict[str, RenderItemWithLinks] + links: List[Link] + + +@define +class stacRenderExtension(FactoryExtension): + """Add /renders endpoint to a STAC TilerFactory.""" + + def register(self, factory: MultiBaseTilerFactory): + """Register endpoint to the tiler factory.""" + + def _prepare_render_item( + render_id: str, + render: Dict, + request: Request, + src_path: str, + ) -> Dict: + """Prepare single render item.""" + links = [ + { + "href": factory.url_for( + request, + "STAC Renders metadata", + render_id=render_id, + ) + + "?" + + urlencode({"url": src_path}), + "rel": "self", + "type": "application/json", + "title": f"STAC Renders metadata for {render_id}", + } + ] + + # List of dependencies a `/tile` URL should validate + # Note: Those dependencies should only require Query() inputs + tile_dependencies = [ + factory.reader_dependency, + factory.tile_dependency, + factory.layer_dependency, + factory.dataset_dependency, + factory.process_dependency, + # Image rendering Dependencies + factory.colormap_dependency, + factory.render_dependency, + ] + if check_query_params(tile_dependencies, render): + query_string = urlencode({"url": src_path, **render}, doseq=True) + + links += [ + { + "href": factory.url_for( + request, + "tilejson", + tileMatrixSetId="{tileMatrixSetId}", + ) + + "?" + + query_string, + "rel": "tilesets-map", + "title": f"tilejson file for {render_id}", + "templated": True, + }, + { + "href": factory.url_for( + request, + "wmts", + tileMatrixSetId="{tileMatrixSetId}", + ) + + "?" + + query_string, + "rel": "tilesets-map", + "title": f"WMTS service for {render_id}", + "templated": True, + }, + ] + + return {"params": render, "links": links} + + @factory.router.get( + "/renders", + response_model=RenderItemList, + response_model_exclude_none=True, + name="List STAC Renders metadata", + operation_id=f"{factory.operation_prefix}getRenderList", + ) + def render_list(request: Request, src_path=Depends(factory.path_dependency)): + with factory.reader(src_path) as src: + renders = src.item.properties.get("renders", {}) + + prepared_renders = { + render_id: _prepare_render_item(render_id, render, request, src_path) + for render_id, render in renders.items() + } + return { + "renders": prepared_renders, + "links": [ + { + "href": str(request.url), + "rel": "self", + "type": "application/json", + "title": "List STAC Renders metadata", + }, + ], + } + + @factory.router.get( + "/renders/{render_id}", + response_model=RenderItemWithLinks, + response_model_exclude_none=True, + name="STAC Renders metadata", + operation_id=f"{factory.operation_prefix}getRender", + ) + def render( + request: Request, + render_id: str = Path( + description="render id", + ), + src_path=Depends(factory.path_dependency), + ): + with factory.reader(src_path) as src: + renders = src.item.properties.get("renders", {}) + + if render_id not in renders: + raise HTTPException(status_code=404, detail="Render not found") + + return _prepare_render_item( + render_id, + renders[render_id], + request, + src_path, + ) diff --git a/src/titiler/extensions/titiler/extensions/stac.py b/src/titiler/extensions/titiler/extensions/stac.py index 6848c7a35..af2723368 100644 --- a/src/titiler/extensions/titiler/extensions/stac.py +++ b/src/titiler/extensions/titiler/extensions/stac.py @@ -1,21 +1,12 @@ """rio-stac Extension.""" -import sys -from dataclasses import dataclass from typing import Any, Dict, List, Literal, Optional +from attrs import define from fastapi import Depends, Query +from typing_extensions import Annotated, TypedDict -from titiler.core.factory import BaseTilerFactory, FactoryExtension - -# Avoids a Pydantic error: -# TypeError: You should use `typing_extensions.TypedDict` instead of `typing.TypedDict` with Python < 3.9.2. -# Without it, there is no way to differentiate required and optional fields when subclassed. -# Ref: https://github.com/pydantic/pydantic/pull/3374 -if sys.version_info < (3, 9, 2): - from typing_extensions import TypedDict -else: - from typing import TypedDict +from titiler.core.factory import FactoryExtension, TilerFactory try: import pystac @@ -42,11 +33,11 @@ class Item(TypedDict, total=False): collection: str -@dataclass +@define class stacExtension(FactoryExtension): """Add /stac endpoint to a COG TilerFactory.""" - def register(self, factory: BaseTilerFactory): + def register(self, factory: TilerFactory): """Register endpoint to the tiler factory.""" assert ( @@ -56,58 +47,93 @@ def register(self, factory: BaseTilerFactory): media = [m.value for m in pystac.MediaType] + ["auto"] - @factory.router.get("/stac", response_model=Item, name="Create STAC Item") + @factory.router.get( + "/stac", + response_model=Item, + name="Create STAC Item", + operation_id=f"{factory.operation_prefix}createSTAC", + ) def create_stac( - src_path: str = Depends(factory.path_dependency), - datetime: Optional[str] = Query( - None, - description="The date and time of the assets, in UTC (e.g 2020-01-01, 2020-01-01T01:01:01).", - ), - extensions: Optional[List[str]] = Query( - None, description="STAC extension URL the Item implements." - ), - collection: Optional[str] = Query( - None, description="The Collection ID that this item belongs to." - ), - collection_url: Optional[str] = Query( - None, description="Link to the STAC Collection." - ), + src_path=Depends(factory.path_dependency), + datetime: Annotated[ + Optional[str], + Query( + description="The date and time of the assets, in UTC (e.g 2020-01-01, 2020-01-01T01:01:01).", + ), + ] = None, + extensions: Annotated[ + Optional[List[str]], + Query(description="STAC extension URL the Item implements."), + ] = None, + collection: Annotated[ + Optional[str], + Query(description="The Collection ID that this item belongs to."), + ] = None, + collection_url: Annotated[ + Optional[str], + Query(description="Link to the STAC Collection."), + ] = None, # properties: Optional[Dict] = Query(None, description="Additional properties to add in the item."), - id: Optional[str] = Query( - None, - description="Id to assign to the item (default to the source basename).", - ), - asset_name: Optional[str] = Query( - "data", description="asset name for the source (default to 'data')." - ), - asset_roles: Optional[List[str]] = Query( - None, description="list of asset's roles." - ), - asset_media_type: Literal[tuple(media)] = Query( # type: ignore - "auto", description="Asset's media type" - ), - asset_href: Optional[str] = Query( - None, description="Asset's URI (default to source's path)" - ), - with_proj: bool = Query( - True, description="Add the `projection` extension and properties." - ), - with_raster: bool = Query( - True, description="Add the `raster` extension and properties." - ), - with_eo: bool = Query( - True, description="Add the `eo` extension and properties." - ), - max_size: Optional[int] = Query( - 1024, - gt=0, - description="Limit array size from which to get the raster statistics.", - ), + id: Annotated[ + Optional[str], + Query( + description="Id to assign to the item (default to the source basename)." + ), + ] = None, + asset_name: Annotated[ + Optional[str], + Query(description="asset name for the source (default to 'data')."), + ] = "data", + asset_roles: Annotated[ + Optional[List[str]], + Query(description="list of asset's roles."), + ] = None, + asset_media_type: Annotated[ # type: ignore + Optional[Literal[tuple(media)]], + Query(description="Asset's media type"), + ] = "auto", + asset_href: Annotated[ + Optional[str], + Query(description="Asset's URI (default to source's path)"), + ] = None, + with_proj: Annotated[ + Optional[bool], + Query(description="Add the `projection` extension and properties."), + ] = True, + with_raster: Annotated[ + Optional[bool], + Query(description="Add the `raster` extension and properties."), + ] = True, + with_eo: Annotated[ + Optional[bool], + Query(description="Add the `eo` extension and properties."), + ] = True, + max_size: Annotated[ + Optional[int], + Query( + gt=0, + description="Limit array size from which to get the raster statistics.", + ), + ] = 1024, + geom_densify_pts: Annotated[ + Optional[int], + Query( + alias="geometry_densify", + ge=0, + description="Number of points to add to each edge to account for nonlinear edges transformation.", + ), + ] = 0, + geom_precision: Annotated[ + Optional[int], + Query( + alias="geometry_precision", + ge=-1, + description="Round geometry coordinates to this number of decimal.", + ), + ] = -1, ): """Create STAC item.""" - properties = ( - {} - ) # or properties = properties or {} if we add properties in Query + properties = {} # or properties = properties or {} if we add properties in Query dt = None if datetime: @@ -138,4 +164,6 @@ def create_stac( with_raster=with_raster, with_eo=with_eo, raster_max_size=max_size, + geom_densify_pts=geom_densify_pts, + geom_precision=geom_precision, ).to_dict() diff --git a/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html b/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html index d5e3c448f..04d37554b 100644 --- a/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html +++ b/src/titiler/extensions/titiler/extensions/templates/cog_viewer.html @@ -4,13 +4,20 @@ TiTiler - Cloud Optimized GeoTIFF Viewer - - - - - - - + + + + + +