diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c52f902..1414edb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Update titiler-pgstac version to `0.1.0.a4` in `pctiler` (https://github.com/microsoft/planetary-computer-apis/pull/46) +- Update titiler-pgstac version to `0.1.0.a4` in `pctiler` [#46](https://github.com/microsoft/planetary-computer-apis/pull/46) +- Move render config, queryables and mosaic info into Azure Storage Tables [#48](https://github.com/microsoft/planetary-computer-apis/pull/48) ### Added - Added support for /queryables endpoint [#44](https://github.com/microsoft/planetary-computer-apis/pull/44) +- Addd `/mosaic/info` endpoint [#48](https://github.com/microsoft/planetary-computer-apis/pull/48) ### Fixed diff --git a/deployment/README.md b/deployment/README.md index 8a3256d0..1550e39e 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -41,3 +41,24 @@ Container Registry repo where you published your local images: - `IMAGE_TAG` __Note:__ Remember to bring down your resources after testing with `terraform destroy`! + +## Loading configuration data + +Configuration data is stored in Azure Storage Tables. Use the `pcapis` command line interface that is installed with the `pccommon` package to load data. For example: + +``` +> pcapis load -t collection --sas "${SAS_TOKEN}" --account pctapissatyasa --table collectionconfig --file pccommon/tests/data-files/collection_config.json +``` +To dump a single collection config, use: + +``` +> pcapis dump -t collection --sas "${SAS_TOKEN}" --account pctapissatyasa --table collectionconfig --id naip +``` + +For container configs, you must also specify the container account name used as the Partition Key: + +``` +> pcapis dump -t collection --sas "${SAS_TOKEN}" --account pctapissatyasa --table containerconfig --id naip --container-account naipeuwest +``` + +Using the `load` command on a single dump file for either config will update the single row. diff --git a/deployment/helm/deploy-values.template.yaml b/deployment/helm/deploy-values.template.yaml index ac50ba70..3b32e221 100644 --- a/deployment/helm/deploy-values.template.yaml +++ b/deployment/helm/deploy-values.template.yaml @@ -12,6 +12,12 @@ stac: maxWorkers: "1" poolSize: "1" + storage: + account_name: {{ tf.storage_account_name }} + account_key: {{ tf.storage_account_key }} + collection_config_table_name: {{ tf.collection_config_table_name }} + container_config_table_name: {{ tf.container_config_table_name }} + deploy: replicaCount: {{ tf.stac_replica_count }} podAnnotations: @@ -39,6 +45,12 @@ tiler: workersPerCore: "1" webConcurrency: "1" + storage: + account_name: {{ tf.storage_account_name }} + account_key: {{ tf.storage_account_key }} + collection_config_table_name: {{ tf.collection_config_table_name }} + container_config_table_name: {{ tf.container_config_table_name }} + deploy: replicaCount: {{ tf.tiler_replica_count }} podAnnotations: diff --git a/deployment/helm/pc-apis-ingress/templates/ingress.yaml b/deployment/helm/pc-apis-ingress/templates/ingress.yaml index 0053d5be..15718cf3 100644 --- a/deployment/helm/pc-apis-ingress/templates/ingress.yaml +++ b/deployment/helm/pc-apis-ingress/templates/ingress.yaml @@ -1,7 +1,7 @@ {{- if .Values.pcingress.ingress.enabled -}} {{- $fullName := include "pcingress.fullname" . -}} {{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1beta1 +apiVersion: networking.k8s.io/v1 {{- else -}} apiVersion: extensions/v1beta1 {{- end }} @@ -26,15 +26,21 @@ spec: paths: {{ if $.Values.stac.enabled }} - path: {{ $.Values.pcingress.services.stac.path }} + pathType: Exact backend: - serviceName: {{ $.Values.pcingress.services.stac.name }} - servicePort: {{ $.Values.pcingress.services.stac.port }} + service: + name: {{ $.Values.pcingress.services.stac.name }} + port: + number: {{ $.Values.pcingress.services.stac.port }} {{- end}} {{ if $.Values.tiler.enabled }} - path: {{ $.Values.pcingress.services.tiler.path }} + pathType: Exact backend: - serviceName: {{ $.Values.pcingress.services.tiler.name }} - servicePort: {{ $.Values.pcingress.services.tiler.port }} + service: + name: {{ $.Values.pcingress.services.tiler.name }} + port: + number: {{ $.Values.pcingress.services.tiler.port }} {{- end}} {{- end }} {{- end }} \ No newline at end of file diff --git a/deployment/helm/published/planetary-computer-stac/templates/deployment.yaml b/deployment/helm/published/planetary-computer-stac/templates/deployment.yaml index e285e8f9..015306a5 100644 --- a/deployment/helm/published/planetary-computer-stac/templates/deployment.yaml +++ b/deployment/helm/published/planetary-computer-stac/templates/deployment.yaml @@ -79,8 +79,20 @@ spec: value: "{{ .Values.environment }}" - name: "PGSSLMODE" value: "require" - - name: "DEBUG" + - name: "PCAPIS_DEBUG" value: "{{ .Values.stac.debug }}" + - name: "PCAPIS_COLLECTION_CONFIG__ACCOUNT_NAME" + value: "{{ .Values.stac.storage.account_name }}" + - name: "PCAPIS_COLLECTION_CONFIG__ACCOUNT_KEY" + value: "{{ .Values.stac.storage.account_key }}" + - name: "PCAPIS_COLLECTION_CONFIG__TABLE_NAME" + value: "{{ .Values.stac.storage.collection_config_table_name }}" + - name: "PCAPIS_CONTAINER_CONFIG__ACCOUNT_NAME" + value: "{{ .Values.stac.storage.account_name }}" + - name: "PCAPIS_CONTAINER_CONFIG__ACCOUNT_KEY" + value: "{{ .Values.stac.storage.account_key }}" + - name: "PCAPIS_CONTAINER_CONFIG__TABLE_NAME" + value: "{{ .Values.stac.storage.container_config_table_name }}" - name: APP_INSIGHTS_INSTRUMENTATION_KEY value: "{{ .Values.metrics.instrumentationKey }}" diff --git a/deployment/helm/published/planetary-computer-stac/values.yaml b/deployment/helm/published/planetary-computer-stac/values.yaml index a1b089e1..00224d20 100644 --- a/deployment/helm/published/planetary-computer-stac/values.yaml +++ b/deployment/helm/published/planetary-computer-stac/values.yaml @@ -17,6 +17,12 @@ stac: port: 80 annotations: {} + storage: + account_name: "" + account_key: "" + collection_config_table_name: "" + container_config_table_name: "" + deploy: replicaCount: 1 podAnnotations: {} diff --git a/deployment/helm/published/planetary-computer-tiler/templates/deployment.yaml b/deployment/helm/published/planetary-computer-tiler/templates/deployment.yaml index eabe1587..3fa6f97c 100644 --- a/deployment/helm/published/planetary-computer-tiler/templates/deployment.yaml +++ b/deployment/helm/published/planetary-computer-tiler/templates/deployment.yaml @@ -79,6 +79,18 @@ spec: value: "{{ .Values.tiler.stac_api_href}}" - name: PC_SDK_SAS_URL value: "{{ .Values.tiler.pc_sdk_sas_url}}" + - name: "PCAPIS_COLLECTION_CONFIG__ACCOUNT_NAME" + value: "{{ .Values.tiler.storage.account_name }}" + - name: "PCAPIS_COLLECTION_CONFIG__ACCOUNT_KEY" + value: "{{ .Values.tiler.storage.account_key }}" + - name: "PCAPIS_COLLECTION_CONFIG__TABLE_NAME" + value: "{{ .Values.tiler.storage.collection_config_table_name }}" + - name: "PCAPIS_CONTAINER_CONFIG__ACCOUNT_NAME" + value: "{{ .Values.tiler.storage.account_name }}" + - name: "PCAPIS_CONTAINER_CONFIG__ACCOUNT_KEY" + value: "{{ .Values.tiler.storage.account_key }}" + - name: "PCAPIS_CONTAINER_CONFIG__TABLE_NAME" + value: "{{ .Values.tiler.storage.container_config_table_name }}" - name: APP_INSIGHTS_INSTRUMENTATION_KEY value: "{{ .Values.metrics.instrumentationKey }}" diff --git a/deployment/helm/published/planetary-computer-tiler/values.yaml b/deployment/helm/published/planetary-computer-tiler/values.yaml index a61c7ff1..735f205b 100644 --- a/deployment/helm/published/planetary-computer-tiler/values.yaml +++ b/deployment/helm/published/planetary-computer-tiler/values.yaml @@ -20,6 +20,12 @@ tiler: port: 80 annotations: {} + storage: + account_name: "" + account_key: "" + collection_config_table_name: "" + container_config_table_name: "" + deploy: replicaCount: 10 podAnnotations: {} diff --git a/deployment/terraform/dev/main.tf b/deployment/terraform/dev/main.tf index b34568cd..7d636838 100644 --- a/deployment/terraform/dev/main.tf +++ b/deployment/terraform/dev/main.tf @@ -2,22 +2,14 @@ variable "username" { type = string } -variable "cluster_cert_issuer" { - type = string - default = "letsencrypt-staging" -} - -variable "cluster_cert_server" { - type = string - default = "https://acme-staging-v02.api.letsencrypt.org/directory" -} - module "resources" { source = "../resources" environment = var.username region = "West Europe" + k8s_version = "1.22.4" + cluster_cert_issuer = "letsencrypt" cluster_cert_server = "https://acme-v02.api.letsencrypt.org/directory" diff --git a/deployment/terraform/resources/acr.tf b/deployment/terraform/resources/acr.tf new file mode 100644 index 00000000..69f6d692 --- /dev/null +++ b/deployment/terraform/resources/acr.tf @@ -0,0 +1,11 @@ +data "azurerm_container_registry" "pc" { + name = var.pc_test_resources_acr + resource_group_name = var.pc_test_resources_rg +} + +# add the role to the identity the kubernetes cluster was assigned +resource "azurerm_role_assignment" "attach_acr" { + scope = data.azurerm_container_registry.pc.id + role_definition_name = "AcrPull" + principal_id = azurerm_kubernetes_cluster.pc.kubelet_identity[0].object_id +} diff --git a/deployment/terraform/resources/aks.tf b/deployment/terraform/resources/aks.tf index 358acf6a..73668b11 100644 --- a/deployment/terraform/resources/aks.tf +++ b/deployment/terraform/resources/aks.tf @@ -3,7 +3,7 @@ resource "azurerm_kubernetes_cluster" "pc" { location = azurerm_resource_group.pc.location resource_group_name = azurerm_resource_group.pc.name dns_prefix = "${local.prefix}-cluster" - kubernetes_version = "1.20.7" + kubernetes_version = var.k8s_version addon_profile { kube_dashboard { diff --git a/deployment/terraform/resources/output.tf b/deployment/terraform/resources/output.tf index a42befe1..6ecbee53 100644 --- a/deployment/terraform/resources/output.tf +++ b/deployment/terraform/resources/output.tf @@ -1,9 +1,9 @@ output "environment" { - value = var.environment + value = var.environment } output "location" { - value = local.location + value = local.location } output "cluster_name" { @@ -33,8 +33,8 @@ output "pg_database" { } output "pg_password" { - value = data.azurerm_key_vault_secret.db_admin_password.value - sensitive = true + value = data.azurerm_key_vault_secret.db_admin_password.value + sensitive = true } # Helm pass-through vars @@ -73,3 +73,21 @@ output "tiler_replica_count" { output "instrumentation_key" { value = azurerm_application_insights.pc_application_insights.instrumentation_key } + +## Storage + +output "storage_account_name" { + value = azurerm_storage_account.pc.name +} + +output "storage_account_key" { + value = azurerm_storage_account.pc.primary_access_key +} + +output "collection_config_table_name" { + value = azurerm_storage_table.collectionconfig.name +} + +output "container_config_table_name" { + value = azurerm_storage_table.containerconfig.name +} diff --git a/deployment/terraform/resources/storage_account.tf b/deployment/terraform/resources/storage_account.tf new file mode 100644 index 00000000..d38d4645 --- /dev/null +++ b/deployment/terraform/resources/storage_account.tf @@ -0,0 +1,19 @@ +resource "azurerm_storage_account" "pc" { + name = "${local.nodash_prefix}sa" + resource_group_name = azurerm_resource_group.pc.name + location = azurerm_resource_group.pc.location + account_tier = "Standard" + account_replication_type = "LRS" +} + +# Tables + +resource "azurerm_storage_table" "collectionconfig" { + name = "collectionconfig" + storage_account_name = azurerm_storage_account.pc.name +} + +resource "azurerm_storage_table" "containerconfig" { + name = "containerconfig" + storage_account_name = azurerm_storage_account.pc.name +} diff --git a/deployment/terraform/resources/variables.tf b/deployment/terraform/resources/variables.tf index e3b39f17..46af569d 100644 --- a/deployment/terraform/resources/variables.tf +++ b/deployment/terraform/resources/variables.tf @@ -16,6 +16,11 @@ variable "pc_test_resources_kv" { default = "pc-test-deploy-secrets" } +variable "pc_test_resources_acr" { + type = string + default = "pccomponentstest" +} + variable "aks_node_count" { type = number } @@ -41,6 +46,10 @@ variable "tiler_replica_count" { type = number } +variable "k8s_version" { + type = string +} + # -- Postgres variable "pg_host" { @@ -76,4 +85,5 @@ locals { stack_id = "pct-apis" location = lower(replace(var.region, " ", "")) prefix = "${local.stack_id}-${local.location}-${var.environment}" + nodash_prefix = replace("${local.stack_id}${var.environment}", "-", "") } diff --git a/deployment/terraform/staging/main.tf b/deployment/terraform/staging/main.tf index 829613ed..b2d85785 100644 --- a/deployment/terraform/staging/main.tf +++ b/deployment/terraform/staging/main.tf @@ -4,6 +4,8 @@ module "resources" { environment = "staging" region = "West Europe" + k8s_version = "1.20.7" + cluster_cert_issuer = "letsencrypt" cluster_cert_server = "https://acme-v02.api.letsencrypt.org/directory" diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index b838d713..1b8349dc 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -13,6 +13,17 @@ services: - POSTGRES_HOST_READER=database - POSTGRES_HOST_WRITER=database - POSTGRES_PORT=5432 + + # Azure Storage + - PCAPIS_COLLECTION_CONFIG__ACCOUNT_URL=http://azurite:10002/devstoreaccount1 + - PCAPIS_COLLECTION_CONFIG__ACCOUNT_NAME=devstoreaccount1 + - PCAPIS_COLLECTION_CONFIG__ACCOUNT_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw== + - PCAPIS_COLLECTION_CONFIG__TABLE_NAME=collectionconfig + + - PCAPIS_CONTAINER_CONFIG__ACCOUNT_URL=http://azurite:10002/devstoreaccount1 + - PCAPIS_CONTAINER_CONFIG__ACCOUNT_NAME=devstoreaccount1 + - PCAPIS_CONTAINER_CONFIG__ACCOUNT_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw== + - PCAPIS_CONTAINER_CONFIG__TABLE_NAME=containerconfig volumes: - .:/opt/src command: > @@ -43,6 +54,17 @@ services: - POSTGRES_PORT=5432 - WEB_CONCURRENCY=1 - WORKERS_PER_CORE=1 + + # Azure Storage + - PCAPIS_COLLECTION_CONFIG__ACCOUNT_URL=http://azurite:10002/devstoreaccount1 + - PCAPIS_COLLECTION_CONFIG__ACCOUNT_NAME=devstoreaccount1 + - PCAPIS_COLLECTION_CONFIG__ACCOUNT_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw== + - PCAPIS_COLLECTION_CONFIG__TABLE_NAME=collectionconfig + + - PCAPIS_CONTAINER_CONFIG__ACCOUNT_URL=http://azurite:10002/devstoreaccount1 + - PCAPIS_CONTAINER_CONFIG__ACCOUNT_NAME=devstoreaccount1 + - PCAPIS_CONTAINER_CONFIG__ACCOUNT_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw== + - PCAPIS_CONTAINER_CONFIG__TABLE_NAME=containerconfig volumes: - .:/opt/src command: > diff --git a/docker-compose.yml b/docker-compose.yml index 3a44ff40..4e1da8d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,9 +10,20 @@ services: - APP_PORT=8081 - FORWARDED_ALLOW_IPS=* - ENVIRONMENT=local - - DEBUG=TRUE + - PCAPIS_DEBUG=TRUE - TILER_HREF=http://localhost:8080/data/ + # Azure Storage + - PCAPIS_COLLECTION_CONFIG__ACCOUNT_URL=http://azurite:10002/devstoreaccount1 + - PCAPIS_COLLECTION_CONFIG__ACCOUNT_NAME=devstoreaccount1 + - PCAPIS_COLLECTION_CONFIG__ACCOUNT_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw== + - PCAPIS_COLLECTION_CONFIG__TABLE_NAME=collectionconfig + + - PCAPIS_CONTAINER_CONFIG__ACCOUNT_URL=http://azurite:10002/devstoreaccount1 + - PCAPIS_CONTAINER_CONFIG__ACCOUNT_NAME=devstoreaccount1 + - PCAPIS_CONTAINER_CONFIG__ACCOUNT_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw== + - PCAPIS_CONTAINER_CONFIG__TABLE_NAME=containerconfig + # Used by pgstac backend - POSTGRES_USER=username - POSTGRES_PASS=password @@ -76,6 +87,17 @@ services: - WEB_CONCURRENCY=1 - WORKERS_PER_CORE=1 + # Azure Storage + - PCAPIS_COLLECTION_CONFIG__ACCOUNT_URL=http://azurite:10002/devstoreaccount1 + - PCAPIS_COLLECTION_CONFIG__ACCOUNT_NAME=devstoreaccount1 + - PCAPIS_COLLECTION_CONFIG__ACCOUNT_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw== + - PCAPIS_COLLECTION_CONFIG__TABLE_NAME=collectionconfig + + - PCAPIS_CONTAINER_CONFIG__ACCOUNT_URL=http://azurite:10002/devstoreaccount1 + - PCAPIS_CONTAINER_CONFIG__ACCOUNT_NAME=devstoreaccount1 + - PCAPIS_CONTAINER_CONFIG__ACCOUNT_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw== + - PCAPIS_CONTAINER_CONFIG__TABLE_NAME=containerconfig + # Used for logging and metrics - APP_INSIGHTS_INSTRUMENTATION_KEY depends_on: @@ -111,8 +133,22 @@ services: - "5432:5432" volumes: - pc-apis-pgdata:/var/lib/postgresql/data + azurite: + container_name: pcapis-azurite + image: mcr.microsoft.com/azure-storage/azurite + hostname: azurite + command: "azurite --silent --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost + 0.0.0.0 -l /workspace" + ports: + - "10000:10000" # Blob + - "10001:10001" # Queue + - "10002:10002" # Table + volumes: + - pc-apis-azurite-data:/workspace + volumes: pc-apis-pgdata: + pc-apis-azurite-data: networks: default: name: pc-apis-dev-network diff --git a/pccommon/pccommon/cdn.py b/pccommon/pccommon/cdn.py new file mode 100644 index 00000000..c8a51dc2 --- /dev/null +++ b/pccommon/pccommon/cdn.py @@ -0,0 +1,23 @@ +import re + +from pccommon.config.core import PCAPIsConfig + +BLOB_REGEX = re.compile(r".*/([^/]+?)\.blob\.core\.windows\.net/([^/]+?).*") + + +class BlobCDN: + @staticmethod + def transform_if_available(asset_href: str) -> str: + m = re.match(BLOB_REGEX, asset_href) + if m: + storage_account = m.group(1) + container = m.group(2) + config = ( + PCAPIsConfig.from_environment() + .get_container_config_table() + .get_config(storage_account, container) + ) + if config and config.has_cdn: + asset_href = asset_href.replace("blob.core.windows", "azureedge") + + return asset_href diff --git a/pccommon/pccommon/cli.py b/pccommon/pccommon/cli.py new file mode 100644 index 00000000..9128bc2a --- /dev/null +++ b/pccommon/pccommon/cli.py @@ -0,0 +1,178 @@ +import argparse +import json +import sys +from typing import Any, Dict, List, Optional + +from pccommon.config.collections import CollectionConfig, CollectionConfigTable +from pccommon.config.containers import ContainerConfig, ContainerConfigTable +from pccommon.constants import DEFAULT_COLLECTION_CONFIG_TABLE_NAME +from pccommon.version import __version__ + + +def load(sas: str, account: str, table: str, type: str, file: str) -> int: + account_url = f"https://{account}.table.core.windows.net" + with open(file) as f: + rows = json.load(f) + + if type == "collection": + col_config_table = CollectionConfigTable.from_sas_token( + account_url=account_url, sas_token=sas, table_name=table + ) + for coll_id, config in rows.items(): + col_config_table.set_config(coll_id, CollectionConfig(**config)) + + elif type == "container": + cont_config_table = ContainerConfigTable.from_sas_token( + account_url=account_url, sas_token=sas, table_name=table + ) + for path, config in rows.items(): + storage_account, container = path.split("/") + cont_config_table.set_config( + storage_account, container, ContainerConfig(**config) + ) + else: + print(f"Unknown type: {type}") + return 1 + + return 0 + + +def dump(sas: str, account: str, table: str, type: str, **kwargs: Any) -> int: + output = kwargs.get("output") + account_url = f"https://{account}.table.core.windows.net" + id = kwargs.get("id") + result: Dict[str, Dict[str, Any]] = {} + if type == "collection": + col_config_table = CollectionConfigTable.from_sas_token( + account_url=account_url, sas_token=sas, table_name=table + ) + + if id: + col_config = col_config_table.get_config(id) + assert col_config + result[id] = col_config.dict() + else: + for (_, collection_id, col_config) in col_config_table.get_all(): + assert collection_id + result[collection_id] = col_config.dict() + + elif type == "container": + con_config_table = ContainerConfigTable.from_sas_token( + account_url=account_url, sas_token=sas, table_name=table + ) + if id: + con_account = kwargs.get("container_account") + assert con_account + con_config = con_config_table.get_config(con_account, id) + assert con_config + result[f"{con_account}/{id}"] = con_config.dict() + else: + for (storage_account, container, con_config) in con_config_table.get_all(): + result[f"{storage_account}/{container}"] = con_config.dict() + else: + print(f"Unknown type: {type}") + return 1 + + if output: + with open(output, "w") as f: + json.dump(result, f, indent=2) + else: + print(json.dumps(result, indent=2)) + + return 0 + + +def parse_args(args: List[str]) -> Optional[Dict[str, Any]]: + desc = "pcapis CLI" + dhf = argparse.ArgumentDefaultsHelpFormatter + parser0 = argparse.ArgumentParser(description=desc) + parser0.add_argument( + "--version", + help="Print version and exit", + action="version", + version=__version__, + ) + + parent = argparse.ArgumentParser(add_help=False) + subparsers = parser0.add_subparsers(dest="command") + + def add_common_opts(p: argparse.ArgumentParser) -> None: + p.add_argument( + "--sas", + help="SAS Token for the storage account.", + required=True, + ) + p.add_argument("--account", help="Storage account name.", required=True) + p.add_argument( + "--table", help="Table name.", default=DEFAULT_COLLECTION_CONFIG_TABLE_NAME + ) + p.add_argument( + "-t", + "--type", + help="Type of configuration.", + choices=["collection", "container"], + required=True, + ) + + # collection config commands + parser = subparsers.add_parser( + "load", + help="Load config into a storage table", + parents=[parent], + formatter_class=dhf, + ) + parser.add_argument( + "--file", help="Filename to load collection configuration from.", required=True + ) + add_common_opts(parser) + + parser = subparsers.add_parser( + "dump", + help="Dump config from a storage table", + parents=[parent], + formatter_class=dhf, + ) + parser.add_argument( + "--output", help="Filename to save collections to", default=None + ) + + parser.add_argument( + "--id", help="Single collection or container id to dump", default=None + ) + parser.add_argument( + "--container-account", + help="Storage account of the specified container config id (PartitionKey)", + default=None, + ) + + add_common_opts(parser) + + parsed_args = { + k: v for k, v in vars(parser0.parse_args(args)).items() if v is not None + } + if "command" not in parsed_args: + parser0.print_usage() + return None + + return parsed_args + + +def cli() -> int: + args = parse_args(sys.argv[1:]) + if not args: + return -1 + + cmd = args.pop("command") + + if cmd == "load": + return load(**args) + elif cmd == "dump": + return dump(**args) + + return 2 + + +if __name__ == "__main__": + return_code = cli() + if return_code and return_code != 0: + sys.exit(return_code) diff --git a/pccommon/pccommon/config.py b/pccommon/pccommon/config.py deleted file mode 100644 index 71abb087..00000000 --- a/pccommon/pccommon/config.py +++ /dev/null @@ -1,37 +0,0 @@ -import logging -import os -from dataclasses import dataclass -from functools import lru_cache -from typing import Optional - -logger = logging.getLogger(__name__) - - -class EnvVars: - # DEBUG mode - DEBUG = "DEBUG" - - # Application Insights instrumentation key for logging via OpenCensus - APP_INSIGHTS_INSTRUMENTATION_KEY = "APP_INSIGHTS_INSTRUMENTATION_KEY" - - -@dataclass -class CommonConfig: - app_insights_instrumentation_key: Optional[str] - debug: bool = os.getenv(EnvVars.DEBUG, "False").lower() == "true" - - @classmethod - @lru_cache - def from_environment(cls) -> "CommonConfig": - app_insights_instrumentation_key = os.getenv( - EnvVars.APP_INSIGHTS_INSTRUMENTATION_KEY - ) - if ( - not app_insights_instrumentation_key - or len(app_insights_instrumentation_key) < 5 - ): - logger.warning("App Insights Instrumentation Key not in environment.") - app_insights_instrumentation_key = None - return cls( - app_insights_instrumentation_key=app_insights_instrumentation_key, - ) diff --git a/pccommon/pccommon/config/__init__.py b/pccommon/pccommon/config/__init__.py new file mode 100644 index 00000000..483ac1c4 --- /dev/null +++ b/pccommon/pccommon/config/__init__.py @@ -0,0 +1,18 @@ +from typing import Optional + +from pccommon.config.collections import CollectionConfig, DefaultRenderConfig +from pccommon.config.core import PCAPIsConfig +from pccommon.utils import map_opt + + +def get_apis_config() -> PCAPIsConfig: + return PCAPIsConfig.from_environment() + + +def get_collection_config(collection_id: str) -> Optional[CollectionConfig]: + table = get_apis_config().get_collection_config_table() + return table.get_config(collection_id) + + +def get_render_config(collection_id: str) -> Optional[DefaultRenderConfig]: + return map_opt(lambda c: c.render_config, get_collection_config(collection_id)) diff --git a/pccommon/pccommon/config/collections.py b/pccommon/pccommon/config/collections.py new file mode 100644 index 00000000..d0e93de5 --- /dev/null +++ b/pccommon/pccommon/config/collections.py @@ -0,0 +1,138 @@ +from typing import Any, Dict, List, Optional + +import orjson +from humps import camelize +from pydantic import BaseModel + +from pccommon.tables import ModelTableService +from pccommon.utils import get_param_str, orjson_dumps + + +class DefaultRenderConfig(BaseModel): + """ + A class used to represent information convenient for accessing + the rendered assets of a collection. + + The parameters stored by this class are not the only parameters + by which rendering is possible or useful but rather represent the + most convenient renderings for human consumption and preview. + For example, if a TIF asset can be viewed as an RGB approximating + normal human vision, parameters will likely encode this rendering. + """ + + render_params: Dict[str, Any] + minzoom: int + assets: Optional[List[str]] = None + maxzoom: Optional[int] = 18 + create_links: bool = True + has_mosaic: bool = False + mosaic_preview_zoom: Optional[int] = None + mosaic_preview_coords: Optional[List[float]] = None + requires_token: bool = False + hidden: bool = False # Hide from API + + def get_full_render_qs(self, collection: str, item: Optional[str] = None) -> str: + """ + Return the full render query string, including the + item, collection, render and assets parameters. + """ + collection_part = f"collection={collection}" if collection else "" + item_part = f"&item={item}" if item else "" + asset_part = self.get_assets_params() + render_part = self.get_render_params() + + return "".join([collection_part, item_part, asset_part, render_part]) + + def get_assets_params(self) -> str: + """ + Convert listed assets to a query string format with multiple `asset` keys + None -> "" + [data1] -> "&asset=data1" + [data1, data2] -> "&asset=data1&asset=data2" + """ + assets = self.assets or [] + keys = ["&assets="] * len(assets) + params = ["".join(item) for item in zip(keys, assets)] + + return "".join(params) + + def get_render_params(self) -> str: + return f"&{get_param_str(self.render_params)}" + + @property + def should_add_collection_links(self) -> bool: + # TODO: has_mosaic flag is legacy from now-deprecated + # sqlite mosaicjson feature. We can reuse this logic + # for injecting Explorer links for collections. Modify + # this logic and resulting STAC links to point to + # the Collection in the Explorer. + return self.has_mosaic and self.create_links and (not self.hidden) + + @property + def should_add_item_links(self) -> bool: + return self.create_links and (not self.hidden) + + class Config: + json_loads = orjson.loads + json_dumps = orjson_dumps + + +class CamelModel(BaseModel): + class Config: + alias_generator = camelize + allow_population_by_field_name = True + json_loads = orjson.loads + json_dumps = orjson_dumps + + +class Mosaics(CamelModel): + name: str + description: Optional[str] = None + cql: List[Dict[str, Any]] + + +class LegendConfig(CamelModel): + type: Optional[str] + labels: Optional[List[str]] + trim_start: Optional[int] + trim_end: Optional[int] + + +class RenderOptions(CamelModel): + name: str + description: Optional[str] = None + options: str + min_zoom: int + legend: Optional[LegendConfig] = None + + +class DefaultLocation(CamelModel): + zoom: int + coordinates: List[float] + + +class MosaicInfo(CamelModel): + mosaics: List[Mosaics] + render_options: List[RenderOptions] + default_location: DefaultLocation + default_custom_query: Optional[Dict[str, Any]] = None + + +class CollectionConfig(BaseModel): + render_config: DefaultRenderConfig + queryables: Dict[str, Any] + mosaic_info: MosaicInfo + + class Config: + json_loads = orjson.loads + json_dumps = orjson_dumps + + +class CollectionConfigTable(ModelTableService[CollectionConfig]): + _model = CollectionConfig + + def get_config(self, collection_id: str) -> Optional[CollectionConfig]: + return self.get("", collection_id) + + def set_config(self, collection_id: str, config: CollectionConfig) -> None: + self.upsert("", collection_id, config) diff --git a/pccommon/pccommon/config/containers.py b/pccommon/pccommon/config/containers.py new file mode 100644 index 00000000..a40875b2 --- /dev/null +++ b/pccommon/pccommon/config/containers.py @@ -0,0 +1,29 @@ +from typing import Optional + +import orjson +from pydantic import BaseModel + +from pccommon.tables import ModelTableService +from pccommon.utils import orjson_dumps + + +class ContainerConfig(BaseModel): + has_cdn: bool = False + + class Config: + json_loads = orjson.loads + json_dumps = orjson_dumps + + +class ContainerConfigTable(ModelTableService[ContainerConfig]): + _model = ContainerConfig + + def get_config( + self, storage_account: str, container: str + ) -> Optional[ContainerConfig]: + return self.get(storage_account, container) + + def set_config( + self, storage_account: str, container: str, config: ContainerConfig + ) -> None: + self.upsert(storage_account, container, config) diff --git a/pccommon/pccommon/config/core.py b/pccommon/pccommon/config/core.py new file mode 100644 index 00000000..d3059648 --- /dev/null +++ b/pccommon/pccommon/config/core.py @@ -0,0 +1,69 @@ +import logging +from typing import Optional + +from cachetools import Cache, LRUCache, cachedmethod +from cachetools.func import lru_cache +from cachetools.keys import hashkey +from pydantic import BaseModel, BaseSettings, Field, PrivateAttr + +from pccommon.config.collections import CollectionConfigTable +from pccommon.config.containers import ContainerConfigTable +from pccommon.constants import DEFAULT_TABLE_TTL + +logger = logging.getLogger(__name__) + + +ENV_VAR_PCAPIS_PREFIX = "PCAPIS_" +APP_INSIGHTS_INSTRUMENTATION_KEY = "APP_INSIGHTS_INSTRUMENTATION_KEY" + + +class TableConfig(BaseModel): + account_name: str + account_key: str + table_name: str + account_url: Optional[str] = None + + +class PCAPIsConfig(BaseSettings): + _cache: Cache = PrivateAttr(default_factory=lambda: LRUCache(maxsize=10)) + + app_insights_instrumentation_key: Optional[str] = Field( # type: ignore + default=None, + env=APP_INSIGHTS_INSTRUMENTATION_KEY, + ) + collection_config: TableConfig + container_config: TableConfig + + table_value_ttl: int = Field(default=DEFAULT_TABLE_TTL) + + debug: bool = False + + @cachedmethod(cache=lambda self: self._cache, key=lambda _: hashkey("collection")) + def get_collection_config_table(self) -> CollectionConfigTable: + return CollectionConfigTable.from_account_key( + account_url=self.collection_config.account_url, + account_name=self.collection_config.account_name, + account_key=self.collection_config.account_key, + table_name=self.collection_config.table_name, + ttl=self.table_value_ttl, + ) + + @cachedmethod(cache=lambda self: self._cache, key=lambda _: hashkey("container")) + def get_container_config_table(self) -> ContainerConfigTable: + return ContainerConfigTable.from_account_key( + account_url=self.container_config.account_url, + account_name=self.container_config.account_name, + account_key=self.container_config.account_key, + table_name=self.container_config.table_name, + ttl=self.table_value_ttl, + ) + + @classmethod + @lru_cache(maxsize=1) + def from_environment(cls) -> "PCAPIsConfig": + return PCAPIsConfig() # type: ignore + + class Config: + env_prefix = ENV_VAR_PCAPIS_PREFIX + extra = "ignore" + env_nested_delimiter = "__" diff --git a/pccommon/pccommon/constants.py b/pccommon/pccommon/constants.py new file mode 100644 index 00000000..45ff08ab --- /dev/null +++ b/pccommon/pccommon/constants.py @@ -0,0 +1,4 @@ +DEFAULT_COLLECTION_CONFIG_TABLE_NAME = "collectionconfig" +DEFAULT_CONTAINER_CONFIG_TABLE_NAME = "containerconfig" + +DEFAULT_TABLE_TTL = 600 # 10 minutes diff --git a/pccommon/pccommon/logging.py b/pccommon/pccommon/logging.py index 27e72a8a..45904e96 100644 --- a/pccommon/pccommon/logging.py +++ b/pccommon/pccommon/logging.py @@ -10,7 +10,7 @@ from fastapi import Request from opencensus.ext.azure.log_exporter import AzureLogHandler -from pccommon.config import CommonConfig +from pccommon.config import get_apis_config class ServiceName: @@ -47,7 +47,7 @@ def filter(self, record: logging.LogRecord) -> bool: # Initialize logging, including a console handler, and sending all logs containing # custom_dimensions to Application Insights def init_logging(service_name: str) -> None: - config = CommonConfig.from_environment() + config = get_apis_config() # Setup logging log_level = logging.DEBUG if config.debug else logging.INFO diff --git a/pccommon/pccommon/middleware.py b/pccommon/pccommon/middleware.py index 564cc504..90cb2b98 100644 --- a/pccommon/pccommon/middleware.py +++ b/pccommon/pccommon/middleware.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Awaitable, Callable, Coroutine +from typing import Awaitable, Callable from fastapi import HTTPException, Request, Response from fastapi.applications import FastAPI diff --git a/pccommon/pccommon/render.py b/pccommon/pccommon/render.py deleted file mode 100644 index 3f268dfa..00000000 --- a/pccommon/pccommon/render.py +++ /dev/null @@ -1,279 +0,0 @@ -import re -from dataclasses import dataclass -from typing import Any, Dict, List, Optional - -from pccommon.utils import get_param_str - - -@dataclass -class DefaultRenderConfig: - """ - A class used to represent information convenient for accessing - the rendered assets of a collection. - - The parameters stored by this class are not the only parameters - by which rendering is possible or useful but rather represent the - most convenient renderings for human consumption and preview. - For example, if a TIF asset can be viewed as an RGB approximating - normal human vision, parameters will likely encode this rendering. - """ - - render_params: Dict[str, Any] - minzoom: int - assets: Optional[List[str]] = None - maxzoom: Optional[int] = 18 - create_links: bool = True - has_mosaic: bool = False - mosaic_preview_zoom: Optional[int] = None - mosaic_preview_coords: Optional[List[float]] = None - requires_token: bool = False - hidden: bool = False # Hide from API - - def get_full_render_qs(self, collection: str, item: Optional[str] = None) -> str: - """ - Return the full render query string, including the - item, collection, render and assets parameters. - """ - collection_part = f"collection={collection}" if collection else "" - item_part = f"&item={item}" if item else "" - asset_part = self.get_assets_params() - render_part = self.get_render_params() - - return "".join([collection_part, item_part, asset_part, render_part]) - - def get_assets_params(self) -> str: - """ - Convert listed assets to a query string format with multiple `asset` keys - None -> "" - [data1] -> "&asset=data1" - [data1, data2] -> "&asset=data1&asset=data2" - """ - assets = self.assets or [] - keys = ["&assets="] * len(assets) - params = ["".join(item) for item in zip(keys, assets)] - - return "".join(params) - - def get_render_params(self) -> str: - return f"&{get_param_str(self.render_params)}" - - @property - def should_add_collection_links(self) -> bool: - # TODO: has_mosaic flag is legacy from now-deprecated - # sqlite mosaicjson feature. We can reuse this logic - # for injecting Explorer links for collections. Modify - # this logic and resulting STAC links to point to - # the Collection in the Explorer. - return self.has_mosaic and self.create_links and (not self.hidden) - - @property - def should_add_item_links(self) -> bool: - return self.create_links and (not self.hidden) - - -# TODO: Should store this in the PQE database as a separate -# table of PC-specific configuration per collection - - -COLLECTION_RENDER_CONFIG = { - "3dep-seamless": DefaultRenderConfig( - assets=["data"], - render_params={"colormap_name": "terrain", "rescale": [-1000, 4000]}, - requires_token=True, - mosaic_preview_zoom=8, - mosaic_preview_coords=[47.1113, -120.8578], - minzoom=8, - ), - "alos-dem": DefaultRenderConfig( - assets=["data"], - render_params={"colormap_name": "terrain", "rescale": [-1000, 4000]}, - mosaic_preview_zoom=8, - mosaic_preview_coords=[35.6837, 138.4281], - requires_token=True, - minzoom=8, - ), - "aster-l1t": DefaultRenderConfig( - assets=["VNIR"], - render_params={"asset_bidx": "VNIR|2,3,1", "nodata": 0}, - mosaic_preview_zoom=9, - mosaic_preview_coords=[37.2141, -104.2947], - minzoom=9, - ), - "chloris-biomass": DefaultRenderConfig( - assets=["data"], - render_params={"colormap_name": "chloris-biomass", "rescale": [1, 750000]}, - has_mosaic=False, - mosaic_preview_zoom=2, - mosaic_preview_coords=[30.0572, 80.1735], - requires_token=True, - minzoom=2, - ), - "cop-dem-glo-30": DefaultRenderConfig( - assets=["data"], - render_params={"colormap_name": "terrain", "rescale": [-1000, 4000]}, - mosaic_preview_zoom=8, - mosaic_preview_coords=[30.0572, 80.1735], - requires_token=True, - minzoom=8, - ), - "cop-dem-glo-90": DefaultRenderConfig( - assets=["data"], - render_params={"colormap_name": "terrain", "rescale": [-1000, 4000]}, - mosaic_preview_zoom=8, - mosaic_preview_coords=[46.8776, 12.1444], - requires_token=True, - minzoom=8, - ), - "gap": DefaultRenderConfig( - assets=["data"], - render_params={"tile_format": "png", "colormap_name": "gap-lulc"}, - mosaic_preview_zoom=7, - mosaic_preview_coords=[26.7409, -80.9714], - requires_token=False, - minzoom=5, - ), - "gnatsgo-rasters": DefaultRenderConfig( - assets=["aws0_100"], - render_params={"colormap_name": "cividis", "rescale": [0, 600]}, - mosaic_preview_zoom=6, - mosaic_preview_coords=[44.1454, -112.6404], - requires_token=True, - minzoom=4, - ), - "goes-mcmip": DefaultRenderConfig( - create_links=True, # Issues with colormap size, rendering - assets=["data"], - render_params={"colormap_name": "gap-lulc"}, - mosaic_preview_zoom=13, - mosaic_preview_coords=[39.95340, -75.16333], - requires_token=True, - minzoom=6, - ), - "goes-cmi": DefaultRenderConfig( - create_links=True, - render_params={ - "expression": ( - "C02_2km_wm," - "0.45*C02_2km_wm+0.1*C03_2km_wm+0.45*C01_2km_wm," - "C01_2km_wm" - ), - "nodata": -1, - "rescale": [1, 1000], - "color_formula": "Gamma RGB 2.5 Saturation 1.4 Sigmoidal RGB 2 0.7", - "resampling": "bilinear", - }, - has_mosaic=True, - mosaic_preview_zoom=4, - mosaic_preview_coords=[33.4872, -114.4842], - requires_token=True, - minzoom=2, - ), - "hgb": DefaultRenderConfig( - assets=["aboveground"], - render_params={"colormap_name": "greens", "nodata": 0, "rescale": [0, 255]}, - mosaic_preview_zoom=9, - mosaic_preview_coords=[-7.641129, 39.162521], - minzoom=2, - ), - "hrea": DefaultRenderConfig( - assets=["estimated-brightness"], - render_params={"colormap_name": "magma", "rescale": [1, 200]}, - mosaic_preview_zoom=3, - mosaic_preview_coords=[11.8280, 20.6367], - minzoom=3, - ), - "io-lulc": DefaultRenderConfig( - assets=["data"], - render_params={"colormap_name": "io-lulc"}, - mosaic_preview_zoom=4, - mosaic_preview_coords=[-0.8749, 109.8456], - minzoom=4, - ), - "io-lulc-9-class": DefaultRenderConfig( - assets=["data"], - render_params={"colormap_name": "io-lulc-9-class"}, - mosaic_preview_zoom=4, - mosaic_preview_coords=[-0.8749, 109.8456], - minzoom=4, - ), - "jrc-gsw": DefaultRenderConfig( - assets=["occurrence"], - render_params={"colormap_name": "jrc-occurrence", "nodata": 0}, - mosaic_preview_zoom=10, - mosaic_preview_coords=[24.21647, 91.015209], - minzoom=4, - ), - "landsat-8-c2-l2": DefaultRenderConfig( - assets=["SR_B4", "SR_B3", "SR_B2"], - render_params={ - "color_formula": "gamma RGB 2.7, saturation 1.5, sigmoidal RGB 15 0.55" - }, - mosaic_preview_zoom=11, - mosaic_preview_coords=[37.4069, 118.8188], - requires_token=True, - minzoom=8, - ), - "mobi": DefaultRenderConfig( - create_links=False, # Couldn't find good visualization - assets=["SpeciesRichness_All"], - render_params={"colormap_name": "magma", "nodata": 128, "rescale": "0,1"}, - requires_token=True, - mosaic_preview_zoom=4, - mosaic_preview_coords=[37.3052, -85.8457], - minzoom=3, - ), - "mtbs": DefaultRenderConfig( - create_links=False, # Issues with COG size and format - assets=["burn-severity"], - render_params={"colormap_name": "mtbs-severity"}, - mosaic_preview_zoom=9, - mosaic_preview_coords=[39.2234, -122.6932], - minzoom=3, - ), - "naip": DefaultRenderConfig( - assets=["image"], - render_params={"asset_bidx": "image|1,2,3"}, - mosaic_preview_zoom=13, - mosaic_preview_coords=[36.0891, -111.8577], - minzoom=11, - ), - "nasadem": DefaultRenderConfig( - assets=["elevation"], - render_params={"colormap_name": "terrain", "rescale": [-100, 4000]}, - mosaic_preview_zoom=7, - mosaic_preview_coords=[-10.7270, -74.7620], - requires_token=False, - minzoom=7, - ), - "nrcan-landcover": DefaultRenderConfig( - assets=["landcover"], - render_params={"colormap_name": "nrcan-lulc"}, - mosaic_preview_zoom=7, - mosaic_preview_coords=[51.3913, -124.8087], - requires_token=True, - minzoom=4, - ), - "sentinel-2-l2a": DefaultRenderConfig( - assets=["visual"], - render_params={"asset_bidx": "visual|1,2,3", "nodata": 0}, - mosaic_preview_zoom=9, - mosaic_preview_coords=[-16.4940, 124.0274], - requires_token=True, - minzoom=9, - ), -} - -STORAGE_ACCOUNTS_WITH_CDNS = set(["naipeuwest"]) - -BLOB_REGEX = re.compile(r".*/([^/]+?)\.blob\.core\.windows\.net.*") - - -class BlobCDN: - @staticmethod - def transform_if_available(asset_href: str) -> str: - m = re.match(BLOB_REGEX, asset_href) - if m: - if m.group(1) in STORAGE_ACCOUNTS_WITH_CDNS: - asset_href = asset_href.replace("blob.core.windows", "azureedge") - - return asset_href diff --git a/pccommon/pccommon/tables.py b/pccommon/pccommon/tables.py new file mode 100644 index 00000000..e609770a --- /dev/null +++ b/pccommon/pccommon/tables.py @@ -0,0 +1,206 @@ +from typing import ( + Any, + Callable, + Dict, + Generic, + Iterable, + Optional, + Tuple, + Type, + TypeVar, +) + +import orjson +from azure.core.credentials import AzureNamedKeyCredential, AzureSasCredential +from azure.core.exceptions import ResourceNotFoundError +from azure.data.tables import TableClient, TableServiceClient +from cachetools import Cache, TTLCache, cachedmethod +from pydantic import BaseModel + +from pccommon.constants import DEFAULT_TABLE_TTL + +T = TypeVar("T", bound="TableService") +M = TypeVar("M", bound=BaseModel) +V = TypeVar("V") + + +class TableConfig(BaseModel): + account_url: str + table_name: str + sas_token: str + + +class TableError(Exception): + pass + + +def encode_model(m: BaseModel) -> str: + return m.json() + + +def decode_dict(s: str) -> Dict[str, Any]: + return orjson.loads(s) + + +class TableService: + def __init__( + self, + get_clients: Callable[[], Tuple[Optional[TableServiceClient], TableClient]], + ttl: Optional[int] = None, + ) -> None: + self._get_clients = get_clients + self._service_client: Optional[TableServiceClient] = None + self._table_client: Optional[TableClient] = None + self._cache: Cache = TTLCache(maxsize=1024, ttl=ttl or DEFAULT_TABLE_TTL) + + def _ensure_table_client(self) -> None: + if not self._table_client: + raise TableError("Table client not initialized. Use as a context manager.") + + def __enter__(self) -> TableClient: + self._service_client, self._table_client = self._get_clients() + return self._table_client + + def __exit__(self, *args: Any) -> None: + if self._table_client: + self._table_client.close() + self._table_client = None + if self._service_client: + self._service_client.close() + self._service_client = None + + @classmethod + def from_sas_token( + cls: Type[T], account_url: str, sas_token: str, table_name: str + ) -> T: + def _get_clients( + _url: str = account_url, _token: str = sas_token, _table: str = table_name + ) -> Tuple[Optional[TableServiceClient], TableClient]: + table_service_client = TableServiceClient( + endpoint=_url, + credential=AzureSasCredential(_token), + ) + return ( + table_service_client, + table_service_client.get_table_client(table_name=_table), + ) + + return cls(_get_clients) + + @classmethod + def from_connection_string( + cls: Type[T], connection_string: str, table_name: str + ) -> T: + def _get_clients( + _conn_str: str = connection_string, _table: str = table_name + ) -> Tuple[Optional[TableServiceClient], TableClient]: + table_service_client = TableServiceClient.from_connection_string( + conn_str=_conn_str + ) + return ( + table_service_client, + table_service_client.get_table_client(table_name=_table), + ) + + return cls(_get_clients) + + @classmethod + def from_account_key( + cls: Type[T], + account_name: str, + account_key: str, + table_name: str, + account_url: Optional[str] = None, + ttl: Optional[int] = None, + ) -> T: + def _get_clients( + _name: str = account_name, + _key: str = account_key, + _url: Optional[str] = account_url, + _table: str = table_name, + ) -> Tuple[Optional[TableServiceClient], TableClient]: + _url = _url or f"https://{_name}.table.core.windows.net" + credential = AzureNamedKeyCredential(name=_name, key=_key) + table_service_client = TableServiceClient( + endpoint=_url, credential=credential + ) + return ( + table_service_client, + table_service_client.get_table_client(table_name=_table), + ) + + return cls(_get_clients, ttl=ttl) + + +class ModelTableService(Generic[M], TableService): + _model: Type[M] + + def _parse_model( + self, entity: Dict[str, Any], partition_key: str, row_key: str + ) -> M: + data: Any = entity.get("Data") + if not data: + raise TableError( + "Data column expected but not found. " + f"partition_key={partition_key} row_key={row_key}" + ) + if not isinstance(data, str): + raise TableError( + "Data column must be a string. " + f"partition_key={partition_key} row_key={row_key}" + ) + return self._model(**decode_dict(data)) + + def insert(self, partition_key: str, row_key: str, entity: M) -> None: + with self as table_client: + table_client.create_entity( + { + "PartitionKey": partition_key, + "RowKey": row_key, + "Data": encode_model(entity), + } + ) + + def upsert(self, partition_key: str, row_key: str, entity: M) -> None: + with self as table_client: + table_client.upsert_entity( + { + "PartitionKey": partition_key, + "RowKey": row_key, + "Data": encode_model(entity), + } + ) + + def update(self, partition_key: str, row_key: str, entity: M) -> None: + with self as table_client: + table_client.update_entity( + { + "PartitionKey": partition_key, + "RowKey": row_key, + "Data": encode_model(entity), + } + ) + + @cachedmethod(cache=lambda self: self._cache) + def get(self, partition_key: str, row_key: str) -> Optional[M]: + with self as table_client: + try: + entity = table_client.get_entity( + partition_key=partition_key, row_key=row_key + ) + return self._parse_model(entity, partition_key, row_key) + + except ResourceNotFoundError: + return None + + def get_all(self) -> Iterable[Tuple[Optional[str], Optional[str], M]]: + with self as table_client: + for entity in table_client.query_entities(""): + partition_key, row_key = entity.get("PartitionKey"), entity.get( + "RowKey" + ) + yield ( + partition_key, + row_key, + self._parse_model(entity, partition_key, row_key), + ) diff --git a/pccommon/pccommon/tracing.py b/pccommon/pccommon/tracing.py index cfe41ab6..378ec762 100644 --- a/pccommon/pccommon/tracing.py +++ b/pccommon/pccommon/tracing.py @@ -10,10 +10,10 @@ from opencensus.trace.span import SpanKind from opencensus.trace.tracer import Tracer -from pccommon.config import CommonConfig -from pccommon.logging import ServiceName, request_to_path +from pccommon.config import get_apis_config +from pccommon.logging import request_to_path -config = CommonConfig.from_environment() +_config = get_apis_config() logger = logging.getLogger(__name__) @@ -31,10 +31,10 @@ exporter = ( AzureExporter( connection_string=( - f"InstrumentationKey={config.app_insights_instrumentation_key}" + f"InstrumentationKey={_config.app_insights_instrumentation_key}" ) ) - if config.app_insights_instrumentation_key is not None + if _config.app_insights_instrumentation_key is not None else None ) @@ -206,7 +206,8 @@ def _parse_queryjson(query: dict) -> Tuple[Optional[str], Optional[str]]: collection_ids = query.get("collections") item_ids = query.get("ids") - # Collection and ids are List[str] per the spec, but the client may allow just a single item + # Collection and ids are List[str] per the spec, + # but the client may allow just a single item if isinstance(collection_ids, list): collection_ids = ",".join(collection_ids) if isinstance(item_ids, list): diff --git a/pccommon/pccommon/utils.py b/pccommon/pccommon/utils.py index 57a72f3e..0b7799e9 100644 --- a/pccommon/pccommon/utils.py +++ b/pccommon/pccommon/utils.py @@ -1,5 +1,10 @@ import urllib.parse -from typing import Any, Dict +from typing import Any, Callable, Dict, Optional, TypeVar + +import orjson + +T = TypeVar("T") +U = TypeVar("U") def get_param_str(params: Dict[str, Any]) -> str: @@ -9,3 +14,15 @@ def transform(v: Any) -> str: return urllib.parse.quote_plus(str(v)) return "&".join([f"{k}={transform(v)}" for k, v in params.items()]) + + +def map_opt(fn: Callable[[T], U], v: Optional[T]) -> Optional[U]: + """Maps the value of an option to another value, returning + None if the input option is None. + """ + return v if v is None else fn(v) + + +def orjson_dumps(v: Dict[str, Any], *args: Any, default: Any) -> str: + # orjson.dumps returns bytes, to match standard json.dumps we need to decode + return orjson.dumps(v, default=default).decode() diff --git a/pccommon/requirements.txt b/pccommon/requirements.txt deleted file mode 100644 index 4cc4d07c..00000000 --- a/pccommon/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -fastapi==0.67.* -opencensus-ext-azure==1.0.8 -opencensus-ext-logging==0.1.0 diff --git a/pccommon/setup.py b/pccommon/setup.py index a4b663a1..9ac6d41b 100644 --- a/pccommon/setup.py +++ b/pccommon/setup.py @@ -3,8 +3,18 @@ from setuptools import find_packages, setup # Runtime requirements. -with open("requirements.txt", "r") as f: - inst_reqs = f.readlines() +inst_reqs = [ + "fastapi==0.67.*", + "opencensus-ext-azure==1.0.8", + "opencensus-ext-logging==0.1.0", + "orjson==3.5.2", + "azure-identity==1.7.1", + "azure-data-tables==12.2.0", + "pydantic==1.9.0", + "cachetools==5.0.0", + "types-cachetools==4.2.9", + "pyhumps==3.5.3", +] extra_reqs = { "test": [ @@ -26,4 +36,5 @@ zip_safe=False, install_requires=inst_reqs, extras_require=extra_reqs, + entry_points={"console_scripts": ["pcapis=pccommon.cli:cli"]}, ) diff --git a/pccommon/tests/config/__init__.py b/pccommon/tests/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pccommon/tests/config/test_mosaic_info.py b/pccommon/tests/config/test_mosaic_info.py new file mode 100644 index 00000000..240cdbe8 --- /dev/null +++ b/pccommon/tests/config/test_mosaic_info.py @@ -0,0 +1,126 @@ +from pccommon.config.collections import MosaicInfo + + +def test_parse() -> None: + d = { + "mosaics": [ + { + "name": "Most recent", + "description": "", + "cql": [ + { + "op": "<=", + "args": [{"property": "datetime"}, {"timestamp": "2020-05-06"}], + } + ], + } + ], + "renderOptions": [ + { + "name": "Elevation (terrain)", + "description": "Elevation...", + "options": "assets=data&rescale=-1000,4000&colormap_name=terrain", + "minZoom": 8, + }, + { + "name": "Elevation (viridis)", + "description": "Elevation...", + "options": "assets=data&rescale=-1000,4000&colormap_name=viridis", + "minZoom": 8, + }, + { + "name": "Elevation (gray)", + "description": "Elevation data scaled -1000 to 4000 with gray colormap", + "options": "assets=data&rescale=-1000,4000&colormap_name=gray_r", + "minZoom": 8, + }, + ], + "defaultLocation": {"zoom": 8, "coordinates": [47.1113, -120.8578]}, + } + model = MosaicInfo.parse_obj(d) + serialized = model.dict(by_alias=True, exclude_unset=True) + + assert d == serialized + + +def test_parse_with_legend() -> None: + d = { + "mosaics": [ + { + "name": "Most recent", + "description": "", + "cql": [ + { + "op": "=", + "args": [{"property": "datetime"}, {"timestamp": "2020-07-01"}], + } + ], + } + ], + "renderOptions": [ + { + "name": "Water occurrence", + "description": "Shows where surface water...", + "options": "assets=occurrence&colormap_name=jrc-occurrence&nodata=0", + "minZoom": 4, + "legend": { + "type": "continuous", + "labels": ["Sometimes water", "Always water"], + "trimStart": 1, + }, + }, + { + "name": "Annual water recurrence", + "description": "Shows frequency that water...", + "options": "assets=recurrence&colormap_name=jrc-recurrence&nodata=0", + "minZoom": 4, + "legend": { + "type": "continuous", + "labels": [">0%", "100%"], + "trimStart": 1, + "trimEnd": 2, + }, + }, + { + "name": "Water Occurrence change intensity", + "description": "Shows where surface water occurrence...", + "options": "assets=change&colormap_name=jrc-change&nodata=0", + "minZoom": 4, + "legend": { + "type": "continuous", + "labels": ["Decrease", "No change", "Increase"], + "trimEnd": 3, + }, + }, + { + "name": "Water seasonality", + "description": "Indicates more seasonal...", + "options": "assets=seasonality&colormap_name=jrc-seasonality&nodata=0", + "minZoom": 4, + "legend": { + "type": "continuous", + "labels": ["Seasonal", "Permanent"], + "trimStart": 1, + "trimEnd": 1, + }, + }, + { + "name": "Water transitions", + "description": "Classifies the change in water state...", + "options": "assets=transitions&colormap_name=jrc-transitions&nodata=0", + "minZoom": 4, + }, + { + "name": "Maximum water extent", + "description": "Show the maximum observed water...", + "options": "assets=extent&colormap_name=jrc-extent&nodata=0", + "minZoom": 4, + }, + ], + "defaultLocation": {"zoom": 10, "coordinates": [24.21647, 91.015209]}, + } + + model = MosaicInfo.parse_obj(d) + serialized = model.dict(by_alias=True, exclude_unset=True) + + assert d == serialized diff --git a/pccommon/tests/config/test_render_config.py b/pccommon/tests/config/test_render_config.py new file mode 100644 index 00000000..0b3eb0cc --- /dev/null +++ b/pccommon/tests/config/test_render_config.py @@ -0,0 +1,68 @@ +from urllib.parse import quote_plus + +from pccommon.config import get_render_config +from pccommon.config.collections import DefaultRenderConfig + +multi_assets = DefaultRenderConfig( + assets=["data1", "data2"], + render_params={"colormap_name": "terrain", "rescale": [-1000, 4000]}, + minzoom=8, +) + +single_asset = DefaultRenderConfig( + assets=["data1"], + render_params={"colormap_name": "terrain", "rescale": [-1000, 4000]}, + minzoom=8, +) + +no_assets = DefaultRenderConfig( + render_params={ + "expression": ("asset1," "0.45*asset2," "asset3/asset1"), + "colormap_name": "terrain", + "rescale": [-1000, 4000], + }, + minzoom=8, +) + + +def test_multi_asset() -> None: + qs = multi_assets.get_full_render_qs("my_collection_id", "my_item_id") + assert qs == ( + "collection=my_collection_id&item=my_item_id&" + "assets=data1&assets=data2&colormap_name=terrain&rescale=-1000,4000" + ) + + +def test_single_asset() -> None: + qs = single_asset.get_full_render_qs("my_collection_id", "my_item_id") + assert qs == ( + "collection=my_collection_id&item=my_item_id&assets=data1&" + "colormap_name=terrain&rescale=-1000,4000" + ) + + +def test_no_asset() -> None: + qs = no_assets.get_full_render_qs("my_collection_id", "my_item_id") + encoded_params = quote_plus("asset1,0.45*asset2,asset3/asset1") + assert qs == ( + f"collection=my_collection_id&item=my_item_id&expression={encoded_params}" + "&colormap_name=terrain&rescale=-1000,4000" + ) + + +def test_collection_only() -> None: + qs = single_asset.get_full_render_qs("my_collection_id") + assert qs == ( + "collection=my_collection_id&assets=data1&colormap_name=terrain&" + "rescale=-1000,4000" + ) + + +def test_get_render_config() -> None: + config = get_render_config("naip") + assert config + encoded_params = quote_plus("image|1,2,3") + assert ( + config.get_full_render_qs("naip") + == f"collection=naip&assets=image&asset_bidx={encoded_params}" + ) diff --git a/pccommon/tests/data-files/collection_config.json b/pccommon/tests/data-files/collection_config.json new file mode 100644 index 00000000..701a45d8 --- /dev/null +++ b/pccommon/tests/data-files/collection_config.json @@ -0,0 +1,5225 @@ +{ + "3dep-seamless": { + "render_config": { + "render_params": { + "colormap_name": "terrain", + "rescale": [ + -1000, + 4000 + ] + }, + "minzoom": 8, + "assets": [ + "data" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 8, + "mosaic_preview_coords": [ + 47.1113, + -120.8578 + ], + "requires_token": true, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + }, + "threedep:region": { + "title": "Region", + "type": "string" + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent", + "description": "", + "cql": [ + { + "op": "<=", + "args": [ + { + "property": "datetime" + }, + { + "timestamp": "2020-05-06" + } + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "Elevation (terrain)", + "description": "Elevation data scaled -1000 to 4000 with terrain colormap", + "options": "assets=data&rescale=-1000,4000&colormap_name=terrain", + "minZoom": 8 + }, + { + "name": "Elevation (viridis)", + "description": "Elevation data scaled -1000 to 4000 with viridis colormap", + "options": "assets=data&rescale=-1000,4000&colormap_name=viridis", + "minZoom": 8 + }, + { + "name": "Elevation (gray)", + "description": "Elevation data scaled -1000 to 4000 with gray colormap", + "options": "assets=data&rescale=-1000,4000&colormap_name=gray_r", + "minZoom": 8 + } + ], + "defaultLocation": { + "zoom": 8, + "coordinates": [ + 47.1113, + -120.8578 + ] + } + } + }, + "alos-dem": { + "render_config": { + "render_params": { + "colormap_name": "terrain", + "rescale": [ + -1000, + 4000 + ] + }, + "minzoom": 8, + "assets": [ + "data" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 8, + "mosaic_preview_coords": [ + 35.6837, + 138.4281 + ], + "requires_token": true, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2016-12-07T00:00:00Z", + "2016-12-07T23:59:59Z" + ] + } + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "Elevation (terrain)", + "description": "Elevation data scaled -1000 to 4000 with terrain colormap", + "options": "assets=data&rescale=-1000,4000&colormap_name=terrain", + "minZoom": 8 + }, + { + "name": "Elevation (viridis)", + "description": "Elevation data scaled -1000 to 4000 with viridis colormap", + "options": "assets=data&rescale=-1000,4000&colormap_name=viridis", + "minZoom": 8 + }, + { + "name": "Elevation (gray)", + "description": "Elevation data scaled -1000 to 4000 with gray colormap", + "options": "assets=data&rescale=-1000,4000&colormap_name=gray_r", + "minZoom": 8 + } + ], + "defaultLocation": { + "zoom": 8, + "coordinates": [ + 35.6837, + 138.4281 + ] + } + } + }, + "aster-l1t": { + "render_config": { + "render_params": { + "asset_bidx": "VNIR|2,3,1", + "nodata": 0 + }, + "minzoom": 9, + "assets": [ + "VNIR" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 9, + "mosaic_preview_coords": [ + 37.2141, + -104.2947 + ], + "requires_token": false, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "Queryables for Planetary Computer Landsat 8 C2 L2 STAC API", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + }, + "eo:cloud_cover": { + "$ref": "https://stac-extensions.github.io/eo/v1.0.0/schema.json#/definitions/fields/properties/eo:cloud_cover" + }, + "view:off_nadir": { + "$ref": "https://stac-extensions.github.io/view/v1.0.0/schema.json#/definitions/fields/properties/view:off_nadir" + }, + "view:sun_azimuth": { + "$ref": "https://stac-extensions.github.io/view/v1.0.0/schema.json#/definitions/fields/properties/view:sun_azimuth" + }, + "view:sun_elevation": { + "$ref": "https://stac-extensions.github.io/view/v1.0.0/schema.json#/definitions/fields/properties/view:sun_elevation" + }, + "sat:orbit_state": { + "title": "Orbit State", + "$ref": "https://stac-extensions.github.io/sat/v1.0.0/schema.json#/definitions/fields/properties/sat:orbit_state" + }, + "aster:processing_number": { + "title": "Processing #", + "type": "string" + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent (low cloud)", + "description": "", + "cql": [ + { + "op": "<=", + "args": [ + { + "property": "datetime" + }, + { + "timestamp": "2006-12-31T23:59:59Z" + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Most recent (any cloud cover)", + "description": "", + "cql": [ + { + "op": "<=", + "args": [ + { + "property": "datetime" + }, + { + "timestamp": "2006-12-31T23:59:59Z" + } + ] + } + ] + }, + { + "name": "Oct \u2013 Dec 2006 (low-cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2006-10-01", + "2006-12-31T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 20 + ] + } + ] + }, + { + "name": "Jul \u2013 Sep 2006 (low-cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2006-07-01", + "2006-09-30T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 20 + ] + } + ] + }, + { + "name": "Apr \u2013 Jun 2006 (low-cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2006-04-01", + "2006-06-30T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 20 + ] + } + ] + }, + { + "name": "Jan \u2013 Mar 2006 (low-cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2006-01-01", + "2006-03-31T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 20 + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "SWIR - Pseudo RGB", + "description": "SWIR spectral bands 1,3,5", + "options": "assets=SWIR&asset_bidx=SWIR|1,3,5&nodata=0&skipcovered=False&exitwhenfull=False&items_limit=15&time_limit=5&color_formula=gamma RGB 2.7, saturation 1.2, sigmoidal RGB 15 0.55", + "minZoom": 9 + }, + { + "name": "VNIR - Pseudo RGB", + "description": "VNIR spectral bands 2,3,1", + "options": "assets=VNIR&asset_bidx=VNIR|2,3,1&nodata=0&skipcovered=False&exitwhenfull=False&items_limit=15&time_limit=5&items_limit=15", + "minZoom": 9 + } + ], + "defaultLocation": { + "zoom": 9, + "coordinates": [ + 37.2141, + -104.2947 + ] + }, + "defaultCustomQuery": {} + } + }, + "chloris-biomass": { + "render_config": { + "render_params": { + "colormap_name": "chloris-biomass", + "rescale": [ + 1, + 750000 + ] + }, + "minzoom": 2, + "assets": [ + "data" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 2, + "mosaic_preview_coords": [ + 30.0572, + 80.1735 + ], + "requires_token": true, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "All years", + "description": "", + "cql": [] + }, + { + "name": "2019", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2018-08-01", + "2019-01-01" + ] + } + ] + } + ] + }, + { + "name": "2018", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2017-08-01", + "2018-01-01" + ] + } + ] + } + ] + }, + { + "name": "2017", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2016-08-01", + "2017-01-01" + ] + } + ] + } + ] + }, + { + "name": "2016", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2015-08-01", + "2016-01-01" + ] + } + ] + } + ] + }, + { + "name": "2015", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2014-08-01", + "2015-01-01" + ] + } + ] + } + ] + }, + { + "name": "2014", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2013-08-01", + "2014-01-01" + ] + } + ] + } + ] + }, + { + "name": "2013", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2012-08-01", + "2013-01-01" + ] + } + ] + } + ] + }, + { + "name": "2012", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2011-08-01", + "2012-01-01" + ] + } + ] + } + ] + }, + { + "name": "2011", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2010-08-01", + "2011-01-01" + ] + } + ] + } + ] + }, + { + "name": "2010", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2009-08-01", + "2010-01-01" + ] + } + ] + } + ] + }, + { + "name": "2009", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2008-08-01", + "2009-01-01" + ] + } + ] + } + ] + }, + { + "name": "2008", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2007-08-01", + "2008-01-01" + ] + } + ] + } + ] + }, + { + "name": "2007", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2006-08-01", + "2007-01-01" + ] + } + ] + } + ] + }, + { + "name": "2006", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2005-08-01", + "2006-01-01" + ] + } + ] + } + ] + }, + { + "name": "2005", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2004-08-01", + "2005-01-01" + ] + } + ] + } + ] + }, + { + "name": "2004", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2003-08-01", + "2004-01-01" + ] + } + ] + } + ] + }, + { + "name": "2003", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2002-08-01", + "2003-01-01" + ] + } + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "Aboveground Biomass (tonnes)", + "description": "Annual estimates of aboveground woody biomass", + "options": "assets=biomass_wm&colormap_name=chloris-biomass&rescale=1,750000", + "minZoom": 2, + "legend": { + "type": "continuous", + "labels": [ + "0", + "375k", + "> 750k" + ] + } + }, + { + "name": "Biomass Change from prior year (tonnes)", + "description": "Annual estimates of changes (gains and losses) in aboveground woody biomass.", + "options": "assets=biomass_change_wm&colormap_name=spectral&rescale=-5000,5000", + "minZoom": 2 + } + ], + "defaultLocation": { + "zoom": 4, + "coordinates": [ + -4.3028, + -58.0357 + ] + } + } + }, + "cop-dem-glo-30": { + "render_config": { + "render_params": { + "colormap_name": "terrain", + "rescale": [ + -1000, + 4000 + ] + }, + "minzoom": 8, + "assets": [ + "data" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 8, + "mosaic_preview_coords": [ + 30.0572, + 80.1735 + ], + "requires_token": true, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent", + "description": "", + "cql": [ + { + "op": "=", + "args": [ + { + "property": "datetime" + }, + { + "timestamp": "2021-04-22" + } + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "Elevation (terrain)", + "description": "Elevation data scaled -1000 to 4000 with terrain colormap", + "options": "assets=data&rescale=-1000,4000&colormap_name=terrain", + "minZoom": 8 + }, + { + "name": "Elevation (viridis)", + "description": "Elevation data scaled -1000 to 4000 with viridis colormap", + "options": "assets=data&rescale=-1000,4000&colormap_name=viridis", + "minZoom": 8 + }, + { + "name": "Elevation (gray)", + "description": "Elevation data scaled -1000 to 4000 with gray colormap", + "options": "assets=data&rescale=-1000,4000&colormap_name=gray_r", + "minZoom": 8 + } + ], + "defaultLocation": { + "zoom": 8, + "coordinates": [ + 30.0572, + 80.1735 + ] + } + } + }, + "cop-dem-glo-90": { + "render_config": { + "render_params": { + "colormap_name": "terrain", + "rescale": [ + -1000, + 4000 + ] + }, + "minzoom": 8, + "assets": [ + "data" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 8, + "mosaic_preview_coords": [ + 46.8776, + 12.1444 + ], + "requires_token": true, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent", + "description": "", + "cql": [ + { + "op": "=", + "args": [ + { + "property": "datetime" + }, + { + "timestamp": "2021-04-22" + } + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "Elevation (terrain)", + "description": "Elevation data scaled -1000 to 4000 with terrain colormap", + "options": "assets=data&rescale=-1000,4000&colormap_name=terrain", + "minZoom": 8 + }, + { + "name": "Elevation (viridis)", + "description": "Elevation data scaled -1000 to 4000 with viridis colormap", + "options": "assets=data&rescale=-1000,4000&colormap_name=viridis", + "minZoom": 8 + }, + { + "name": "Elevation (gray)", + "description": "Elevation data scaled -1000 to 4000 with gray colormap", + "options": "assets=data&rescale=-1000,4000&colormap_name=gray_r", + "minZoom": 8 + } + ], + "defaultLocation": { + "zoom": 8, + "coordinates": [ + 46.8776, + 12.1444 + ] + } + } + }, + "gap": { + "render_config": { + "render_params": { + "tile_format": "png", + "colormap_name": "gap-lulc" + }, + "minzoom": 5, + "assets": [ + "data" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 7, + "mosaic_preview_coords": [ + 26.7409, + -80.9714 + ], + "requires_token": false, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "1998-01-01", + "2012-01-01" + ] + } + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "Default", + "description": "Land cover classification using custom GAP colormap", + "options": "assets=data&exitwhenfull=False&skipcovered=False&colormap_name=gap-lulc", + "minZoom": 5 + } + ], + "defaultLocation": { + "zoom": 7, + "coordinates": [ + 26.7409, + -80.9714 + ] + } + } + }, + "gnatsgo-rasters": { + "render_config": { + "render_params": { + "colormap_name": "cividis", + "rescale": [ + 0, + 600 + ] + }, + "minzoom": 4, + "assets": [ + "aws0_100" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 6, + "mosaic_preview_coords": [ + 44.1454, + -112.6404 + ], + "requires_token": true, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent", + "cql": [] + } + ], + "renderOptions": [ + { + "name": "Available water storage estimate, aws0_100 (mm)", + "description": "Available water storage estimate (aws) in standard zone 4 (0-100 cm depth), expressed in mm. the volume of plant available water that the soil can store in this zone based on all map unit components (weighted average). null values are presented where data are incomplete or not available.", + "options": "assets=aws0_100&rescale=0.0,600.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Available water storage estimate, aws0_150 (mm)", + "description": "Available water storage estimate (aws) in standard zone 5 (0-150 cm depth), expressed in mm. the volume of plant available water that the soil can store in this zone based on all map unit components (weighted average). null values are presented where data are incomplete or not available.", + "options": "assets=aws0_150&rescale=0.0,879.1&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Available water storage estimate, aws0_20 (mm)", + "description": "Available water storage estimate (aws) in standard zone 2 (0-20 cm depth), expressed in mm. the volume of plant available water that the soil can store in this zone based on all map unit components (weighted average). null values are presented where data are incomplete or not available.", + "options": "assets=aws0_20&rescale=0.0,120.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Available water storage estimate, aws0_30 (mm)", + "description": "Available water storage estimate (aws) in standard zone 3 (0-30 cm depth), expressed in mm. the volume of plant available water that the soil can store in this zone based on all map unit components (weighted average). null values are presented where data are incomplete or not available.", + "options": "assets=aws0_30&rescale=0.0,180.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Available water storage estimate, aws0_5 (mm)", + "description": "Available water storage estimate (aws) in a standard zone 1 (0-5 cm depth), expressed in mm. the volume of plant available water that the soil can store in this layer based on all map unit components (weighted average). null values are presented where data are incomplete or not available.", + "options": "assets=aws0_5&rescale=0.0,30.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Available water storage estimate, aws0_999 (mm)", + "description": "Available water storage estimate (aws) in total soil profile (0 cm to the reported depth of the soil profile), expressed in mm. the volume of plant available water that the soil can store in this layer based on all map unit components (weighted average). null values are presented where data are incomplete or not available.", + "options": "assets=aws0_999&rescale=0.0,1350.39&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Available water storage estimate, aws100_150 (mm)", + "description": "Available water storage estimate (aws) in standard layer 5 (100-150 cm depth), expressed in mm. the volume of plant available water that the soil can store in this layer based on all map unit components (weighted average). null values are presented where data are incomplete or not available.", + "options": "assets=aws100_150&rescale=0.0,287.42&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Available water storage estimate, aws150_999 (mm)", + "description": "Available water storage estimate (aws) in standard layer 6 (150 cm to the reported depth of the soil profile), expressed in mm. the volume of plant available water that the soil can store in this layer based on all map unit components (weighted average). null values are presented where data are incomplete or not available.", + "options": "assets=aws150_999&rescale=0.0,600.1&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Available water storage estimate, aws20_50 (mm)", + "description": "Available water storage estimate (aws) in standard layer 3 (20-50 cm depth), expressed in mm. the volume of plant available water that the soil can store in this layer based on all map unit components (weighted average). null values are presented where data are incomplete or not available.", + "options": "assets=aws20_50&rescale=0.0,180.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Available water storage estimate, aws50_100 (mm)", + "description": "Available water storage estimate (aws) in standard layer 3 (50-100 cm depth), expressed in mm. the volume of plant available water that the soil can store in this layer based on all map unit components (weighted average). null values are presented where data are incomplete or not available.", + "options": "assets=aws50_100&rescale=0.0,300.55&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Available water storage estimate, aws5_20 (mm)", + "description": "Available water storage estimate (aws) in standard layer 2 (5-20 cm depth), expressed in mm. the volume of plant available water that the soil can store in this layer based on all map unit components (weighted average). null values are presented where data are incomplete or not available.", + "options": "assets=aws5_20&rescale=0.0,90.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "National commodity crop productivity index for corn, nccpi3corn", + "description": "National commodity crop productivity index for corn (weighted average) for major earthy components. values range from .01 (low productivity) to .99 (high productivity). earthy components are those soil series or higher level taxa components that can support crop growth (dobos et al., 2012). major components are those soil components where the majorcompflag = 'yes' (ssurgo component table). null values are presented where data are incomplete or not available.", + "options": "assets=nccpi3corn&rescale=0.0,0.991&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "National commodity crop productivity index for cotton, nccpi3cot", + "description": "National commodity crop productivity index for cotton (weighted average) for major earthy components. values range from .01 (low productivity) to .99 (high productivity). earthy components are those soil series or higher level taxa components that can support crop growth (dobos et al., 2012). major components are those soil components where the majorcompflag = 'yes' (ssurgo component table). null values are presented where data are incomplete or not available.", + "options": "assets=nccpi3cot&rescale=0.0,0.901&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "National commodity crop productivity index for small grains, nccpi3sg", + "description": "National commodity crop productivity index for small grains (weighted average) for major earthy components. values range from .01 (low productivity) to .99 (high productivity). earthy components are those soil series or higher level taxa components that can support crop growth (dobos et al., 2012). major components are those soil components where the majorcompflag = 'yes' (ssurgo component table). null values are presented where data are incomplete or not available.", + "options": "assets=nccpi3sg&rescale=0.0,0.983&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "National commodity crop productivity index for soybeans, nccpi3soy", + "description": "National commodity crop productivity index for soybeans (weighted average) for major earthy components. values range from .01 (low productivity) to .99 (high productivity). earthy components are those soil series or higher level taxa components that can support crop growth (dobos et al., 2012). major components are those soil components where the majorcompflag = 'yes' (ssurgo component table). null values are presented where data are incomplete or not available.", + "options": "assets=nccpi3soy&rescale=0.0,0.981&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "National commodity crop productivity index that has the highest value among corn and soybeans, small grains, or cotton, nccpi3all", + "description": "National commodity crop productivity index that has the highest value among corn and soybeans, small grains, or cotton (weighted average) for major earthy components. values range from .01 (low productivity) to .99 (high productivity). earthy components are those soil series or higher level taxa components that can support crop growth (dobos et al., 2012). major components are those soil components where the majorcompflag = 'yes' (ssurgo component table). null values are presented where data are incomplete or not available.", + "options": "assets=nccpi3all&rescale=0.0,0.991&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Potential wetland soil landscapes, pwsl1pomu", + "description": "Potential wetland soil landscapes (pwsl) is expressed as the percentage of the map unit that meets the pwsl criteria. the hydric rating (soil component variable \u201chydricrating\u201d) is an indicator of wet soils. for version 1 (pwsl1), those soil components that meet the following criteria are tagged as pwsl and their comppct_r values are summed for each map unit. soil components with hydricrating = 'yes' are considered pwsl. soil components with hydricrating = \u201cno\u201d are not pwsl. soil components with hydricrating = 'unranked' are tested using other attributes, and will be considered pwsl if any of the following conditions are met: drainagecl = 'poorly drained' or 'very poorly drained' or the localphase or the otherph data fields contain any of the phrases \"drained\" or \"undrained\" or \"channeled\" or \"protected\" or \"ponded\" or \"flooded\". if these criteria do not determine the pwsl for a component and hydricrating = 'unranked', then the map unit will be classified as pwsl if the map unit name contains any of the phrases \"drained\" or \"undrained\" or \"channeled\" or \"protected\" or \"ponded\" or \"flooded\". for version 1 (pwsl1), waterbodies are identified as \"999\" when map unit names match a list of terms that identify water or intermittent water or map units have a sum of the comppct_r for \"water\" that is 80% or greater. null values are presented where data are incomplete or not available.", + "options": "assets=pwsl1pomu&rescale=1,999&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Root zone depth is the depth within the soil profile that commodity crop, rootznemc", + "description": "Root zone depth is the depth within the soil profile that commodity crop (cc) roots can effectively extract water and nutrients for growth. root zone depth influences soil productivity significantly. soil component horizon criteria for root-limiting depth include: presence of hard bedrock, soft bedrock, a fragipan, a duripan, sulfuric material, a dense layer, a layer having a ph of less than 3.5, or a layer having an electrical conductivity of more than 12 within the component soil profile. if no root-restricting zone is identified, a depth of 150 cm is used to approximate the root zone depth (dobos et al., 2012). root zone depth is computed for all map unit major earthy components (weighted average). earthy components are those soil series or higher level taxa components that can support crop growth (dobos et al., 2012). major components are those soil components where the majorcompflag = 'yes' (ssurgo component table). null values are presented where data are incomplete or not available.", + "options": "assets=rootznemc&rescale=0,150&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Root zone, rootznaws (mm)", + "description": "Root zone (commodity crop) available water storage estimate (rzaws) , expressed in mm, is the volume of plant available water that the soil can store within the root zone based on all map unit earthy major components (weighted average). earthy components are those soil series or higher level taxa components that can support crop growth (dobos et al., 2012). major components are those soil components where the majorcompflag = 'yes' (ssurgo component table). null values are presented where data are incomplete or not available.", + "options": "assets=rootznaws&rescale=0,900&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Soil organic carbon stock estimate, soc0_100 (g/m2)", + "description": "Soil organic carbon stock estimate (soc) in standard zone 4 (0-100 cm depth). the concentration of organic carbon present in the soil expressed in grams c per square meter to a depth of 100 cm. null values are presented where data are incomplete or not available.", + "options": "assets=soc0_100&rescale=0.0,802441.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Soil organic carbon stock estimate, soc0_150 (g/m2)", + "description": "Soil organic carbon stock estimate (soc) in standard zone 5 (0-150 cm depth). the concentration of organic carbon present in the soil expressed in grams c per square meter to a depth of 150 cm. null values are presented where data are incomplete or not available.", + "options": "assets=soc0_150&rescale=0.0,1178783.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Soil organic carbon stock estimate, soc0_20 (g/m2)", + "description": "Soil organic carbon stock estimate (soc) in standard zone 2 (0-20 cm depth). the concentration of organic carbon present in the soil expressed in grams c per square meter to a depth of 20 cm. null values are presented where data are incomplete or not available.", + "options": "assets=soc0_20&rescale=0.0,160518.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Soil organic carbon stock estimate, soc0_30 (g/m2)", + "description": "Soil organic carbon stock estimate (soc) in standard zone 3 (0-30 cm depth). the concentration of organic carbon present in the soil expressed in grams c per square meter to a depth of 30 cm. null values are presented where data are incomplete or not available.", + "options": "assets=soc0_30&rescale=0.0,240770.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Soil organic carbon stock estimate, soc0_5 (g/m2)", + "description": "Soil organic carbon stock estimate (soc) in standard layer 1 or standard zone 1 (0-5 cm depth). the concentration of organic carbon present in the soil expressed in grams c per square meter to a depth of 5 cm. null values are presented where data are incomplete or not available.", + "options": "assets=soc0_5&rescale=0.0,40130.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Soil organic carbon stock estimate, soc0_999 (g/m2)", + "description": "Soil organic carbon stock estimate (soc) in total soil profile (0 cm to the reported depth of the soil profile). the concentration of organic carbon present in the soil expressed in grams c per square meter for the total reported soil profile depth. null values are presented where data are incomplete or not available.", + "options": "assets=soc0_999&rescale=0.0,1182344.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Soil organic carbon stock estimate, soc100_150 (g/m2)", + "description": "Soil organic carbon stock estimate (soc) in standard layer 5 (100-150 cm depth). the concentration of organic carbon present in the soil expressed in grams c per square meter for the 100-150 cm layer. null values are presented where data are incomplete or not available.", + "options": "assets=soc100_150&rescale=0.0,376342.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Soil organic carbon stock estimate, soc150_999 (g/m2)", + "description": "Soil organic carbon stock estimate (soc) in standard layer 6 (150 cm to the reported depth of the soil profile). the concentration of organic carbon present in the soil expressed in grams c per square meter for the 150 cm and greater depth layer. null values are presented where data are incomplete or not available.", + "options": "assets=soc150_999&rescale=0.0,126247.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Soil organic carbon stock estimate, soc20_50 (g/m2)", + "description": "Soil organic carbon stock estimate (soc) in standard layer 3 (20-50 cm depth). the concentration of organic carbon present in the soil expressed in grams c per square meter for the 20-50 cm layer. null values are presented where data are incomplete or not available.", + "options": "assets=soc20_50&rescale=0.0,240743.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Soil organic carbon stock estimate, soc50_100 (g/m2)", + "description": "Soil organic carbon stock estimate (soc) in standard layer 4 (50-100 cm depth). the concentration of organic carbon present in the soil expressed in grams c per square meter for the 50-100 cm layer. null values are presented where data are incomplete or not available.", + "options": "assets=soc50_100&rescale=0.0,401179.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Soil organic carbon stock estimate, soc5_20 (g/m2)", + "description": "Soil organic carbon stock estimate (soc) in standard layer 2 (5-20 cm depth). the concentration of organic carbon present in the soil expressed in grams c per square meter for the 5-20 cm layer. null values are presented where data are incomplete or not available.", + "options": "assets=soc5_20&rescale=0.0,120389.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "The national commodity crop productivity index map unit percent earthy is the map unit summed comppct_r for major earthy comps. earthy comps are those soil series or higher level taxa comps that can support crop growth, pctearthmc", + "description": "The national commodity crop productivity index map unit percent earthy is the map unit summed comppct_r for major earthy components. earthy components are those soil series or higher level taxa components that can support crop growth (dobos et al., 2012). major components are those soil components where the majorcompflag = 'yes' (ssurgo component table). useful metadata information. null values are presented where data are incomplete or not available.", + "options": "assets=pctearthmc&rescale=0,100&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "The sum of the comppct_r, musumcpct", + "description": "The sum of the comppct_r (ssurgo component table) values for all listed components in the map unit. useful metadata information. null values are presented where data are incomplete or not available.", + "options": "assets=musumcpct&rescale=0,110&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "The sum of the comppct_r, musumcpcta", + "description": "The sum of the comppct_r (ssurgo component table) values used in the available water storage calculation for the map unit. useful metadata information. null values are presented where data are incomplete or not available.", + "options": "assets=musumcpcta&rescale=1,110&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "The sum of the comppct_r, musumcpcts", + "description": "The sum of the comppct_r (ssurgo component table) values used in the soil organic carbon calculation for the map unit. useful metadata information. null values are presented where data are incomplete or not available.", + "options": "assets=musumcpcts&rescale=1,110&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std layer 1 or std zone 1, tk0_5a (cm)", + "description": "Thickness of soil components used in standard layer 1 or standard zone 1 (0-5 cm) expressed in cm (weighted average) for the available water storage calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk0_5a&rescale=0.05,5.5&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std layer 1 or std zone 1, tk0_5s (cm)", + "description": "Thickness of soil components used in standard layer 1 or standard zone 1 (0-5 cm) expressed in cm (weighted average) for the soil organic carbon calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk0_5s&rescale=0.0,6.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std layer 2, tk5_20a (cm)", + "description": "Thickness of soil components used in standard layer 2 (5-20 cm) expressed in cm (weighted average) for the available water storage calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk5_20a&rescale=0.03,16.5&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std layer 2, tk5_20s (cm)", + "description": "Thickness of soil components used in standard layer 2 (5-20 cm) expressed in cm (weighted average) for the soil organic carbon calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk5_20s&rescale=0.0,17.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std layer 3, tk20_50a (cm)", + "description": "Thickness of soil components used in standard layer 3 (20-50 cm) expressed in cm (weighted average) for the available water storage calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk20_50a&rescale=0.06,37.65&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std layer 3, tk20_50s (cm)", + "description": "Thickness of soil components used in standard layer 3 (20-50 cm) expressed in cm (weighted average) for the soil organic carbon calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk20_50s&rescale=0.0,38.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std layer 4, tk50_100a (cm)", + "description": "Thickness of soil components used in standard layer 4 (50-100 cm) expressed in cm (weighted average) for the available water storage calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk50_100a&rescale=0.03,55.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std layer 4, tk50_100s (cm)", + "description": "Thickness of soil components used in standard layer 4 (50-100 cm) expressed in cm (weighted average) for the soil organic carbon calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk50_100s&rescale=0.0,55.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std layer 5, tk100_150a (cm)", + "description": "Thickness of soil components used in standard layer 5 (100-150 cm) expressed in cm (weighted average) for the available water storage calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk100_150a&rescale=0.04,55.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std layer 5, tk100_150s (cm)", + "description": "Thickness of soil components used in standard layer 5 (100-150 cm) expressed in cm (weighted average) for the soil organic carbon calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk100_150s&rescale=0.0,55.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std layer 6, tk150_999a (cm)", + "description": "Thickness of soil components used in standard layer 6 (150 cm to the reported depth of the soil profile) expressed in cm (weighted average) for the available water storage calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk150_999a&rescale=0.0,307.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std layer 6, tk150_999s (cm)", + "description": "Thickness of soil components used in standard layer 6 (150 cm to the reported depth of the soil profile) expressed in cm (weighted average) for the soil organic carbon calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk150_999s&rescale=0.0,307.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std zone 2, tk0_20a (cm)", + "description": "Thickness of soil components used in standard zone 2 (0-20 cm) expressed in cm (weighted average) for the available water storage calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk0_20a&rescale=0.08,22.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std zone 2, tk0_20s (cm)", + "description": "Thickness of soil components used in standard zone 2 (0-20 cm) expressed in cm (weighted average) for the soil organic carbon calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk0_20s&rescale=0.0,22.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std zone 3, tk0_30a (cm)", + "description": "Thickness of soil components used in standard zone 3 (0-30 cm) expressed in cm (weighted average) for the available water storage calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk0_30a&rescale=0.08,33.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std zone 3, tk0_30s (cm)", + "description": "Thickness of soil components used in standard zone 3 (0-30 cm) expressed in cm (weighted average) for the soil organic carbon calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk0_30s&rescale=0.0,33.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std zone 4, tk0_100a (cm)", + "description": "Thickness of soil components used in standard zone 4 (0-100 cm) expressed in cm (weighted average) for the available water storage calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk0_100a&rescale=0.08,110.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std zone 4, tk0_100s (cm)", + "description": "Thickness of soil components used in standard zone 4 (0-100 cm) expressed in cm (weighted average) for the soil organic carbon calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk0_100s&rescale=0.0,110.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std zone 5, tk0_150a (cm)", + "description": "Thickness of soil components used in standard zone 5 (0-150 cm) expressed in cm (weighted average) for the available water storage calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk0_150a&rescale=0.08,165.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in std zone 5, tk0_150s (cm)", + "description": "Thickness of soil components used in standard zone 5 (0-150 cm) expressed in cm (weighted average) for the soil organic carbon calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk0_150s&rescale=0.0,165.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in total soil profile, tk0_999a (cm)", + "description": "Thickness of soil components used in total soil profile (0 cm to the reported depth of the soil profile) expressed in cm (weighted average) for the available water storage calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk0_999a&rescale=0.08,457.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Thickness of soil comps used in total soil profile, tk0_999s (cm)", + "description": "Thickness of soil components used in total soil profile (0 cm to the reported depth of the soil profile) expressed in cm (weighted average) for the soil organic carbon calculation. null values are presented where data are incomplete or not available.", + "options": "assets=tk0_999s&rescale=0.0,457.0&colormap_name=cividis", + "minZoom": 4 + }, + { + "name": "Zone for commodity crops that is \u2264 6 inches, droughty", + "description": "Zone for commodity crops that is less than or equal to 6 inches (152 mm) expressed as \"1\" for a drought vulnerable soil landscape map unit or \"0\" for a non-droughty soil landscape map unit or null for miscellaneous areas (includes water bodies) or where data were not available. it is computed as a weighted average for major earthy components. earthy components are those soil series or higher level taxa components that can support crop growth (dobos et al., 2012). major components are those soil components where the majorcompflag = 'yes'", + "options": "assets=droughty&rescale=0,1&colormap_name=cividis", + "minZoom": 4 + } + ], + "defaultLocation": { + "zoom": 6, + "coordinates": [ + 44.1454, + -112.6404 + ] + } + } + }, + "goes-mcmip": { + "render_config": { + "render_params": { + "colormap_name": "gap-lulc" + }, + "minzoom": 6, + "assets": [ + "data" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 13, + "mosaic_preview_coords": [ + 39.9534, + -75.16333 + ], + "requires_token": true, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "Queryables for Planetary Computer GOES-R CMI STAC API", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + }, + "platform": { + "description": "Platform", + "type": "string", + "title": "Platform", + "enum": [ + "GOES-16", + "GOES-17" + ] + }, + "goes:image-type": { + "description": "Image type", + "type": "string", + "title": "Image Type", + "enum": [ + "FULL DISK", + "CONUS", + "MESOSCALE" + ] + }, + "goes:mode": { + "title": "Scanning Mode", + "type": "string", + "enum": [ + "3", + "4", + "6" + ] + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent", + "description": "", + "cql": [] + } + ], + "renderOptions": [ + { + "name": "Fire Temperature", + "description": "", + "options": "assets=Temp&rescale=0,3000&colormap_name=magma", + "minZoom": 10 + } + ], + "defaultLocation": { + "zoom": 4, + "coordinates": [ + 33.4872, + -114.4842 + ] + } + } + }, + "goes-cmi": { + "render_config": { + "render_params": { + "expression": "C02_2km_wm,0.45*C02_2km_wm+0.1*C03_2km_wm+0.45*C01_2km_wm,C01_2km_wm", + "nodata": -1, + "rescale": [ + 1, + 1000 + ], + "color_formula": "Gamma RGB 2.5 Saturation 1.4 Sigmoidal RGB 2 0.7", + "resampling": "bilinear" + }, + "minzoom": 2, + "assets": null, + "maxzoom": 18, + "create_links": true, + "has_mosaic": true, + "mosaic_preview_zoom": 4, + "mosaic_preview_coords": [ + 33.4872, + -114.4842 + ], + "requires_token": true, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "Queryables for Planetary Computer GOES-R CMI STAC API", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + }, + "platform": { + "description": "Platform", + "type": "string", + "title": "Platform", + "enum": [ + "GOES-16", + "GOES-17" + ] + }, + "goes:image-type": { + "description": "Image type", + "type": "string", + "title": "Image Type", + "enum": [ + "FULL DISK", + "CONUS", + "MESOSCALE" + ] + }, + "goes:mode": { + "title": "Scanning Mode", + "type": "string", + "enum": [ + "3", + "4", + "6" + ] + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent (Full disk)", + "description": "", + "cql": [ + { + "op": "=", + "args": [ + { + "property": "goes:image-type" + }, + "FULL DISK" + ] + } + ] + }, + { + "name": "Most recent (CONUS)", + "description": "", + "cql": [ + { + "op": "=", + "args": [ + { + "property": "goes:image-type" + }, + "CONUS" + ] + } + ] + }, + { + "name": "Most recent (Mesoscale)", + "description": "", + "cql": [ + { + "op": "=", + "args": [ + { + "property": "goes:image-type" + }, + "MESOSCALE" + ] + } + ] + }, + { + "name": "Most recent (any type)", + "description": "", + "cql": [] + } + ], + "renderOptions": [ + { + "name": "Natural color", + "description": "Natural color representation with Green represented by formula: `0.45*C02_2km+0.1*C03_2km+0.45*C01_2km`", + "options": "expression=C02_2km_wm,0.45*C02_2km_wm+0.1*C03_2km_wm+0.45*C01_2km_wm,C01_2km_wm&nodata=-1&rescale=1,2000&color_formula=Gamma RGB 2.5 Saturation 1.4 Sigmoidal RGB 2 0.7", + "minZoom": 2, + "legend": { + "type": "none" + } + }, + { + "name": "Natural color (low contrast)", + "description": "Lower contrast natural color representation with Green represented by formula: `0.45*C02_2km+0.1*C03_2km+0.45*C01_2km`", + "options": "expression=C02_2km_wm,0.45*C02_2km_wm+0.1*C03_2km_wm+0.45*C01_2km_wm,C01_2km_wm&nodata=-1&rescale=1,3000&color_formula=Gamma RGB 2.5 Saturation 1.4 Sigmoidal RGB 2 0.7", + "minZoom": 2, + "legend": { + "type": "none" + } + } + ], + "defaultLocation": { + "zoom": 4, + "coordinates": [ + 33.4872, + -114.4842 + ] + }, + "defaultCustomQuery": {} + } + }, + "hgb": { + "render_config": { + "render_params": { + "colormap_name": "greens", + "nodata": 0, + "rescale": [ + 0, + 255 + ] + }, + "minzoom": 2, + "assets": [ + "aboveground" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 9, + "mosaic_preview_coords": [ + -7.641129, + 39.162521 + ], + "requires_token": false, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2010-12-31T00:00:00Z", + "2010-12-31T23:59:59Z" + ] + } + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "Above ground biomass carbon density", + "description": "", + "options": "assets=aboveground&colormap_name=greens&nodata=0&rescale=0,255", + "minZoom": 2 + }, + { + "name": "Below ground biomass carbon density", + "description": "", + "options": "assets=belowground&colormap_name=greens&nodata=0&rescale=0,255", + "minZoom": 2 + }, + { + "name": "Above ground biomass - accumulated uncertainty", + "description": "", + "options": "assets=aboveground_uncertainty&colormap_name=reds&nodata=0&rescale=0,50", + "minZoom": 2 + }, + { + "name": "Below ground biomass - accumulated uncertainty", + "description": "", + "options": "assets=belowground_uncertainty&colormap_name=reds&nodata=0&rescale=0,50", + "minZoom": 2 + } + ], + "defaultLocation": { + "zoom": 9, + "coordinates": [ + -7.641129, + 39.162521 + ] + } + } + }, + "hrea": { + "render_config": { + "render_params": { + "colormap_name": "magma", + "rescale": [ + 1, + 200 + ] + }, + "minzoom": 3, + "assets": [ + "estimated-brightness" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 3, + "mosaic_preview_coords": [ + 11.828, + 20.6367 + ], + "requires_token": false, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent (2019)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2019-01-01", + "2019-12-31" + ] + } + ] + } + ] + }, + { + "name": "2018", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2018-01-01", + "2018-12-31" + ] + } + ] + } + ] + }, + { + "name": "2017", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2017-01-01", + "2017-12-31" + ] + } + ] + } + ] + }, + { + "name": "2016", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2016-01-01", + "2016-12-31" + ] + } + ] + } + ] + }, + { + "name": "2015", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2015-01-01", + "2015-12-31" + ] + } + ] + } + ] + }, + { + "name": "2014", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2014-01-01", + "2014-12-31" + ] + } + ] + } + ] + }, + { + "name": "2013", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2013-01-01", + "2013-12-31" + ] + } + ] + } + ] + }, + { + "name": "2012", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2012-01-01", + "2012-12-31" + ] + } + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "Nighttime light composite", + "description": "", + "options": "assets=light-composite&colormap_name=magma&rescale=0.9,2&skipcovered=False&exitwhenfull=False", + "minZoom": 3 + }, + { + "name": "Estimated brightness", + "description": "", + "options": "assets=estimated-brightness&colormap_name=magma&rescale=0,200&skipcovered=False&exitwhenfull=False", + "minZoom": 3 + }, + { + "name": "Probability of electrification", + "description": "", + "options": "assets=lightscore&colormap_name=magma&rescale=0,1&skipcovered=False&exitwhenfull=False", + "minZoom": 3 + }, + { + "name": "Brightness proportion to uninhabited areas", + "description": "Proportion of nights a settlement is brighter than uninhabited areas", + "options": "assets=night-proportion&colormap_name=magma&rescale=0,1&skipcovered=False&exitwhenfull=False", + "minZoom": 3 + } + ], + "defaultLocation": { + "zoom": 3, + "coordinates": [ + 11.828, + 20.6367 + ] + }, + "defaultCustomQuery": {} + } + }, + "io-lulc": { + "render_config": { + "render_params": { + "colormap_name": "io-lulc" + }, + "minzoom": 4, + "assets": [ + "data" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 4, + "mosaic_preview_coords": [ + -0.8749, + 109.8456 + ], + "requires_token": false, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + }, + "io:supercell_id": { + "title": "Supercell ID", + "type": "string" + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent", + "description": "Most recent Land Use/Land Cover", + "cql": [ + { + "op": "=", + "args": [ + { + "property": "datetime" + }, + { + "timestamp": "2020-06-01" + } + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "Default", + "description": "Land cover classification using 10 class custom colormap", + "options": "assets=data&exitwhenfull=False&skipcovered=False&colormap_name=io-lulc", + "minZoom": 4 + } + ], + "defaultLocation": { + "zoom": 4, + "coordinates": [ + -0.8749, + 109.8456 + ] + } + } + }, + "io-lulc-9-class": { + "render_config": { + "render_params": { + "colormap_name": "io-lulc-9-class" + }, + "minzoom": 4, + "assets": [ + "data" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 4, + "mosaic_preview_coords": [ + -0.8749, + 109.8456 + ], + "requires_token": false, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + }, + "io:supercell_id": { + "title": "Supercell ID", + "type": "string" + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "2019", + "description": "2019 Use/Land Cover", + "cql": [ + { + "op": "=", + "args": [ + { + "property": "datetime" + }, + { + "timestamp": "2019-06-01" + } + ] + } + ] + }, + { + "name": "2018", + "description": "2018 Land Use/Land Cover", + "cql": [ + { + "op": "=", + "args": [ + { + "property": "datetime" + }, + { + "timestamp": "2018-06-01" + } + ] + } + ] + }, + { + "name": "2017", + "description": "2017 Land Use/Land Cover", + "cql": [ + { + "op": "=", + "args": [ + { + "property": "datetime" + }, + { + "timestamp": "2017-06-01" + } + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "Default", + "description": "Land cover classification using 9 class custom colormap", + "options": "assets=data&exitwhenfull=False&skipcovered=False&colormap_name=io-lulc-9-class", + "minZoom": 4 + } + ], + "defaultLocation": { + "zoom": 4, + "coordinates": [ + -0.8749, + 109.8456 + ] + } + } + }, + "jrc-gsw": { + "render_config": { + "render_params": { + "colormap_name": "jrc-occurrence", + "nodata": 0 + }, + "minzoom": 4, + "assets": [ + "occurrence" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 10, + "mosaic_preview_coords": [ + 24.21647, + 91.015209 + ], + "requires_token": false, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent", + "description": "", + "cql": [ + { + "op": "=", + "args": [ + { + "property": "datetime" + }, + { + "timestamp": "2020-07-01" + } + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "Water occurrence", + "description": "Shows where surface water occurred between 1984 and 2020", + "options": "assets=occurrence&colormap_name=jrc-occurrence&nodata=0", + "minZoom": 4, + "legend": { + "type": "continuous", + "labels": [ + "Sometimes water", + "Always water" + ], + "trimStart": 1 + } + }, + { + "name": "Annual water recurrence", + "description": "Shows frequency that water returns from one year to another", + "options": "assets=recurrence&colormap_name=jrc-recurrence&nodata=0", + "minZoom": 4, + "legend": { + "type": "continuous", + "labels": [ + ">0%", + "100%" + ], + "trimStart": 1, + "trimEnd": 2 + } + }, + { + "name": "Water Occurrence change intensity", + "description": "Shows where surface water occurrence increased, decreased, or did not change between 1984 and 2020", + "options": "assets=change&colormap_name=jrc-change&nodata=0", + "minZoom": 4, + "legend": { + "type": "continuous", + "labels": [ + "Decrease", + "No change", + "Increase" + ], + "trimEnd": 3 + } + }, + { + "name": "Water seasonality", + "description": "Indicates more seasonal or permanent surface water features during the course of a year", + "options": "assets=seasonality&colormap_name=jrc-seasonality&nodata=0", + "minZoom": 4, + "legend": { + "type": "continuous", + "labels": [ + "Seasonal", + "Permanent" + ], + "trimStart": 1, + "trimEnd": 1 + } + }, + { + "name": "Water transitions", + "description": "Classifies the change in water state between the first and last years of observation", + "options": "assets=transitions&colormap_name=jrc-transitions&nodata=0", + "minZoom": 4 + }, + { + "name": "Maximum water extent", + "description": "Show the maximum observed water extent during the course of 1984 to 2020", + "options": "assets=extent&colormap_name=jrc-extent&nodata=0", + "minZoom": 4 + } + ], + "defaultLocation": { + "zoom": 10, + "coordinates": [ + 24.21647, + 91.015209 + ] + } + } + }, + "landsat-8-c2-l2": { + "render_config": { + "render_params": { + "color_formula": "gamma RGB 2.7, saturation 1.5, sigmoidal RGB 15 0.55" + }, + "minzoom": 8, + "assets": [ + "SR_B4", + "SR_B3", + "SR_B2" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 11, + "mosaic_preview_coords": [ + 37.4069, + 118.8188 + ], + "requires_token": true, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "Queryables for Planetary Computer Landsat 8 C2 L2 STAC API", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + }, + "eo:cloud_cover": { + "$ref": "https://stac-extensions.github.io/eo/v1.0.0/schema.json#/definitions/fields/properties/eo:cloud_cover" + }, + "view:off_nadir": { + "$ref": "https://stac-extensions.github.io/view/v1.0.0/schema.json#/definitions/fields/properties/view:off_nadir" + }, + "view:sun_azimuth": { + "$ref": "https://stac-extensions.github.io/view/v1.0.0/schema.json#/definitions/fields/properties/view:sun_azimuth" + }, + "view:sun_elevation": { + "$ref": "https://stac-extensions.github.io/view/v1.0.0/schema.json#/definitions/fields/properties/view:sun_elevation" + }, + "landsat:wrs_row": { + "title": "WRS Row", + "type": "string" + }, + "landsat:wrs_path": { + "title": "WRS Path", + "type": "string" + }, + "landsat:scene_id": { + "title": "Scene ID", + "type": "string" + }, + "landsat:cloud_cover_land": { + "title": "Cloud Cover Land", + "type": "number", + "minimum": 0, + "maximum": 100 + }, + "landsat:collection_category": { + "title": "Collection Cat.", + "type": "string", + "enum": [ + "T1", + "T2" + ] + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent (low cloud)", + "description": "", + "cql": [ + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Most recent (any cloud cover)", + "description": "", + "cql": [] + }, + { + "name": "Sep \u2013 Nov, 2021 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2021-09-01", + "2021-11-30T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Jun \u2013 Aug, 2021 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2021-06-01", + "2021-08-31T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Mar \u2013 May, 2021 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2021-03-01", + "2021-05-31T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Dec \u2013 Feb, 2021 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2020-12-01", + "2021-02-28T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Sep \u2013 Nov, 2020 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2020-09-01", + "2020-11-30T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Jun \u2013 Aug, 2020 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2020-06-01", + "2020-08-31T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Mar \u2013 May, 2020 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2020-03-01", + "2020-05-31T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Dec \u2013 Feb, 2020 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2019-12-01", + "2020-02-28T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Sep \u2013 Nov, 2019 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2019-09-01", + "2019-11-30T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Jun \u2013 Aug, 2019 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2019-06-01", + "2019-08-31T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Mar \u2013 May, 2019 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2019-03-01", + "2019-05-31T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Dec \u2013 Feb, 2019 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2018-12-01", + "2019-02-28T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Sep \u2013 Nov, 2018 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2018-09-01", + "2018-11-30T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Jun \u2013 Aug, 2018 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2018-06-01", + "2018-08-31T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Mar \u2013 May, 2018 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2018-03-01", + "2018-05-31T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Dec \u2013 Feb, 2018 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2017-12-01", + "2018-02-28T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "Natural color", + "description": "True color composite of visible bands (SR_B4, SR_B3, SR_B2)", + "options": "assets=SR_B4&assets=SR_B3&assets=SR_B2&nodata=0&color_formula=gamma RGB 2.7, saturation 1.5, sigmoidal RGB 15 0.55", + "minZoom": 8 + }, + { + "name": "Color infrared", + "description": "Highlights healthy (red) and unhealthy (blue/gray) vegetation (SR_B5, SR_B4, SR_B3).", + "options": "assets=SR_B5&assets=SR_B4&assets=SR_B3&nodata=0&color_formula=gamma RGB 2.7, saturation 1.5, sigmoidal RGB 15 0.55", + "minZoom": 8 + }, + { + "name": "Short wave infrared", + "description": "Darker shades of green indicate denser vegetation. Brown is indicative of bare soil and built-up areas (SR_B7, SR_B6, SR_B4).", + "options": "assets=SR_B7&assets=SR_B6&assets=SR_B4&nodata=0", + "minZoom": 8 + }, + { + "name": "Agriculture", + "description": "Darker shades of green indicate denser vegetation (SR_B6, SR_B5, SR_B2).", + "options": "assets=SR_B6&assets=SR_B5&assets=SR_B2&nodata=0", + "minZoom": 8 + }, + { + "name": "Normalized Difference Veg. Index (NDVI)", + "description": "Normalized Difference Vegetation Index (SR_B5-SR_B4)/(SR_B5+SR_B4), darker green indicates healthier vegetation.", + "options": "nodata=0&expression=(SR_B5-SR_B4)/(SR_B5+SR_B4)&rescale=-1,1&colormap_name=rdylgn", + "minZoom": 8 + }, + { + "name": "Moisture Index", + "description": "Moisture index indicating water stress in plants (SR_B5-SR_B6)/(SR_B5+SR_B6)", + "options": "nodata=0&expression=(SR_B5-SR_B6)/(SR_B5+SR_B6)&rescale=-1,1&colormap_name=rdbu", + "minZoom": 8 + }, + { + "name": "Atmospheric penetration", + "description": "False color rendering with non-visible bands to reduce effects of atmospheric particles (SR_B7, SR_B6, SR_B5).", + "options": "nodata=0&assets=SR_B7&assets=SR_B6&assets=SR_B5&color_formula=gamma RGB 2.7, saturation 1.5, sigmoidal RGB 15 0.55", + "minZoom": 8 + } + ], + "defaultLocation": { + "zoom": 11, + "coordinates": [ + 37.4069, + 118.8188 + ] + }, + "defaultCustomQuery": {} + } + }, + "mobi": { + "render_config": { + "render_params": { + "colormap_name": "magma", + "nodata": 128, + "rescale": "0,1" + }, + "minzoom": 3, + "assets": [ + "SpeciesRichness_All" + ], + "maxzoom": 18, + "create_links": false, + "has_mosaic": false, + "mosaic_preview_zoom": 4, + "mosaic_preview_coords": [ + 37.3052, + -85.8457 + ], + "requires_token": true, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent (2020)", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2020-04-14T00:00:00Z", + "2020-04-14T23:59:59Z" + ] + } + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "Species richness for all species", + "options": "assets=SpeciesRichness_All&colormap_name=magma&rescale=0,14&nodata=128", + "minZoom": 3 + }, + { + "name": "Species richness for vertebrates", + "options": "assets=SpeciesRichness_Vertebrates&colormap_name=magma&rescale=0,11", + "minZoom": 3 + }, + { + "name": "Species richness for aquatic invertebrates", + "options": "assets=SpeciesRichness_AquaticInverts&colormap_name=magma&rescale=0,19&nodata=128", + "minZoom": 3 + }, + { + "name": "Species richness for vascular plants", + "options": "assets=SpeciesRichness_Plants&colormap_name=magma&rescale=0,17", + "minZoom": 3 + }, + { + "name": "Species richness for pollinators", + "options": "assets=SpeciesRichness_PollinatorInverts&colormap_name=magma&rescale=0,4", + "minZoom": 3 + }, + { + "name": "Protection-weighted range-size rarity for all species", + "options": "assets=PWRSR_GAP12_SUM_All&colormap_name=magma&rescale=0,.001", + "minZoom": 3 + }, + { + "name": "Protection-weighted range-size rarity for vertebrates", + "options": "assets=PWRSR_GAP12_SUM_Vertebrates&colormap_name=magma&rescale=0,.001", + "minZoom": 3 + }, + { + "name": "Protection-weighted range-size rarity for aquatic invertebrates", + "options": "assets=PWRSR_GAP12_SUM_AquaticInverts&colormap_name=magma&rescale=0,.001", + "minZoom": 3 + }, + { + "name": "Protection-weighted range-size rarity for vascular plants", + "options": "assets=PWRSR_GAP12_SUM_PlantsInverts&colormap_name=magma&rescale=0,.001", + "minZoom": 3 + }, + { + "name": "Protection-weighted range-size rarity for pollinators", + "options": "assets=PWRSR_GAP12_SUM_PollinatorInverts&colormap_name=magma&rescale=0,.001", + "minZoom": 3 + }, + { + "name": "Range-size rarity for all species", + "description": "", + "options": "assets=RSR_All&colormap_name=magma&rescale=0,.001", + "minZoom": 3 + }, + { + "name": "Range-size rarity for vertebrates", + "description": "", + "options": "assets=RSR_Vertebrates&colormap_name=magma&rescale=0,.001", + "minZoom": 3 + }, + { + "name": "Range-size rarity for aquatic invertebrates", + "description": "", + "options": "assets=RSR_AquaticInverts&colormap_name=magma&rescale=0,.001", + "minZoom": 3 + }, + { + "name": "Range-size rarity for vascular plants", + "description": "", + "options": "assets=RSR_Plants&colormap_name=magma&rescale=0,.001", + "minZoom": 3 + }, + { + "name": "Range-size rarity for pollinators", + "options": "assets=RSR_PollinatorInverts&colormap_name=magma&rescale=0,.001", + "minZoom": 3 + } + ], + "defaultLocation": { + "zoom": 4, + "coordinates": [ + 37.3052, + -85.8457 + ] + } + } + }, + "mtbs": { + "render_config": { + "render_params": { + "colormap_name": "mtbs-severity" + }, + "minzoom": 3, + "assets": [ + "burn-severity" + ], + "maxzoom": 18, + "create_links": false, + "has_mosaic": false, + "mosaic_preview_zoom": 9, + "mosaic_preview_coords": [ + 39.2234, + -122.6932 + ], + "requires_token": false, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "All years", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "1984-12-31", + "2018-12-31" + ] + } + ] + } + ] + }, + { + "name": "Most recent (2018)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2018-01-01", + "2018-12-31" + ] + } + ] + } + ] + }, + { + "name": "2017", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2017-01-01", + "2017-12-31" + ] + } + ] + } + ] + }, + { + "name": "2016", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2016-01-01", + "2016-12-31" + ] + } + ] + } + ] + }, + { + "name": "2015", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2015-01-01", + "2015-12-31" + ] + } + ] + } + ] + }, + { + "name": "2014", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2014-01-01", + "2014-12-31" + ] + } + ] + } + ] + }, + { + "name": "2013", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2013-01-01", + "2013-12-31" + ] + } + ] + } + ] + }, + { + "name": "2012", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2012-01-01", + "2012-12-31" + ] + } + ] + } + ] + }, + { + "name": "2011", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2011-01-01", + "2011-12-31" + ] + } + ] + } + ] + }, + { + "name": "2010", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2010-01-01", + "2010-12-31" + ] + } + ] + } + ] + }, + { + "name": "2009", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2009-01-01", + "2009-12-31" + ] + } + ] + } + ] + }, + { + "name": "2008", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2008-01-01", + "2008-12-31" + ] + } + ] + } + ] + }, + { + "name": "2007", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2007-01-01", + "2007-12-31" + ] + } + ] + } + ] + }, + { + "name": "2006", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2006-01-01", + "2006-12-31" + ] + } + ] + } + ] + }, + { + "name": "2005", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2005-01-01", + "2005-12-31" + ] + } + ] + } + ] + }, + { + "name": "2004", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2004-01-01", + "2004-12-31" + ] + } + ] + } + ] + }, + { + "name": "2003", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2003-01-01", + "2003-12-31" + ] + } + ] + } + ] + }, + { + "name": "2002", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2002-01-01", + "2002-12-31" + ] + } + ] + } + ] + }, + { + "name": "2001", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2001-01-01", + "2001-12-31" + ] + } + ] + } + ] + }, + { + "name": "2000", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2000-01-01", + "2000-12-31" + ] + } + ] + } + ] + }, + { + "name": "1999", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "1999-01-01", + "1999-12-31" + ] + } + ] + } + ] + }, + { + "name": "1998", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "1998-01-01", + "1998-12-31" + ] + } + ] + } + ] + }, + { + "name": "1997", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "1997-01-01", + "1997-12-31" + ] + } + ] + } + ] + }, + { + "name": "1996", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "1996-01-01", + "1996-12-31" + ] + } + ] + } + ] + }, + { + "name": "1995", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "1995-01-01", + "1995-12-31" + ] + } + ] + } + ] + }, + { + "name": "1994", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "1994-01-01", + "1994-12-31" + ] + } + ] + } + ] + }, + { + "name": "1993", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "1993-01-01", + "1993-12-31" + ] + } + ] + } + ] + }, + { + "name": "1992", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "1992-01-01", + "1992-12-31" + ] + } + ] + } + ] + }, + { + "name": "1991", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "1991-01-01", + "1991-12-31" + ] + } + ] + } + ] + }, + { + "name": "1990", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "1990-01-01", + "1990-12-31" + ] + } + ] + } + ] + }, + { + "name": "1989", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "1989-01-01", + "1989-12-31" + ] + } + ] + } + ] + }, + { + "name": "1988", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "1988-01-01", + "1988-12-31" + ] + } + ] + } + ] + }, + { + "name": "1987", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "1987-01-01", + "1987-12-31" + ] + } + ] + } + ] + }, + { + "name": "1986", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "1986-01-01", + "1986-12-31" + ] + } + ] + } + ] + }, + { + "name": "1985", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "1985-01-01", + "1985-12-31" + ] + } + ] + } + ] + }, + { + "name": "1984", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "1984-12-31", + "1984-12-31" + ] + } + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "Burn severity", + "description": "Classifies the burn severity and extent of large fires across all lands of the United States", + "options": "assets=burn-severity&exitwhenfull=False&skipcovered=True&colormap_name=mtbs-severity", + "minZoom": 3 + } + ], + "defaultLocation": { + "zoom": 9, + "coordinates": [ + 39.2234, + -122.6932 + ] + } + } + }, + "naip": { + "render_config": { + "render_params": { + "asset_bidx": "image|1,2,3" + }, + "minzoom": 11, + "assets": [ + "image" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 13, + "mosaic_preview_coords": [ + 36.0891, + -111.8577 + ], + "requires_token": false, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "Queryables for Planetary Computer Landsat 8 C2 L2 STAC API", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + }, + "naip:year": { + "title": "Year", + "type": "string", + "enum": [ + "2019", + "2018", + "2017", + "2016", + "2015", + "2014", + "2013", + "2012", + "2011", + "2010" + ] + }, + "naip:state": { + "title": "State", + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "wa", + "wv", + "wi", + "wy" + ] + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent available", + "description": "", + "cql": [] + }, + { + "name": "2019", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2019-01-01", + "2019-12-31T23:59:59Z" + ] + } + ] + } + ] + }, + { + "name": "2018", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2018-01-01", + "2018-12-31T23:59:59Z" + ] + } + ] + } + ] + }, + { + "name": "2017", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2017-01-01", + "2017-12-31T23:59:59Z" + ] + } + ] + } + ] + }, + { + "name": "2016", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2016-01-01", + "2016-12-31T23:59:59Z" + ] + } + ] + } + ] + }, + { + "name": "2015", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2015-01-01", + "2015-12-31T23:59:59Z" + ] + } + ] + } + ] + }, + { + "name": "2014", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2014-01-01", + "2014-12-31T23:59:59Z" + ] + } + ] + } + ] + }, + { + "name": "2013", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2013-01-01", + "2013-12-31T23:59:59Z" + ] + } + ] + } + ] + }, + { + "name": "2012", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2012-01-01", + "2012-12-31T23:59:59Z" + ] + } + ] + } + ] + }, + { + "name": "2011", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2011-01-01", + "2011-12-31T23:59:59Z" + ] + } + ] + } + ] + }, + { + "name": "2010", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2010-01-01", + "2010-12-31T23:59:59Z" + ] + } + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "Natural color", + "description": "RGB from visual assets", + "options": "assets=image&asset_bidx=image|1,2,3", + "minZoom": 12 + }, + { + "name": "Color infrared", + "description": "Highlights healthy (red) and unhealthy (blue/gray) vegetation.", + "options": "assets=image&asset_bidx=image|4,1,2&color_formula=Sigmoidal RGB 15 0.35", + "minZoom": 12 + }, + { + "name": "Normalized Difference Veg. Index (NDVI)", + "description": "Normalized Difference Vegetation Index (NIR-Red)/(NIR+Red), darker green indicates healthier vegetation.", + "options": "assets=image&asset_expression=image|(B4-B1)/(B4+B1)&rescale=-1,1&colormap_name=rdylgn", + "minZoom": 12 + } + ], + "defaultLocation": { + "zoom": 13, + "coordinates": [ + 36.0891, + -111.8577 + ] + }, + "defaultCustomQuery": { + "op": "args" + } + } + }, + "nasadem": { + "render_config": { + "render_params": { + "colormap_name": "terrain", + "rescale": [ + -100, + 4000 + ] + }, + "minzoom": 7, + "assets": [ + "elevation" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 7, + "mosaic_preview_coords": [ + -10.727, + -74.762 + ], + "requires_token": false, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent", + "cql": [ + { + "op": "=", + "args": [ + { + "property": "datetime" + }, + { + "timestamp": "2000-02-20" + } + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "Elevation (terrain)", + "description": "Elevation data scaled -1000 to 4000 with terrain colormap", + "options": "assets=elevation&rescale=-1000,4000&colormap_name=terrain", + "minZoom": 7 + }, + { + "name": "Elevation (viridis)", + "description": "Elevation data scaled -1000 to 4000 with viridis colormap", + "options": "assets=elevation&rescale=-1000,4000&colormap_name=viridis", + "minZoom": 7 + }, + { + "name": "Elevation (gray)", + "description": "Elevation data scaled -1000 to 4000 with gray colormap", + "options": "assets=elevation&rescale=-1000,4000&colormap_name=gray_r", + "minZoom": 7 + } + ], + "defaultLocation": { + "zoom": 7, + "coordinates": [ + -10.727, + -74.762 + ] + } + } + }, + "nrcan-landcover": { + "render_config": { + "render_params": { + "colormap_name": "nrcan-lulc" + }, + "minzoom": 4, + "assets": [ + "landcover" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 7, + "mosaic_preview_coords": [ + 51.3913, + -124.8087 + ], + "requires_token": true, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent", + "description": "Most recent Land Use/Land Cover", + "cql": [ + { + "op": ">=", + "args": [ + { + "property": "datetime" + }, + { + "timestamp": "2015-01-01" + } + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "Default", + "description": "Land cover classification using custom colormap", + "options": "assets=landcover&exitwhenfull=False&skipcovered=False&colormap_name=nrcan-lulc", + "minZoom": 4 + } + ], + "defaultLocation": { + "zoom": 7, + "coordinates": [ + 51.3913, + -124.5087 + ] + } + } + }, + "sentinel-2-l2a": { + "render_config": { + "render_params": { + "asset_bidx": "visual|1,2,3", + "nodata": 0 + }, + "minzoom": 9, + "assets": [ + "visual" + ], + "maxzoom": 18, + "create_links": true, + "has_mosaic": false, + "mosaic_preview_zoom": 9, + "mosaic_preview_coords": [ + -16.494, + 124.0274 + ], + "requires_token": true, + "hidden": false + }, + "queryables": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://example.org/queryables", + "type": "object", + "title": "Queryables for PC Sentinel-2-l2a STAC API", + "description": "Queryable properties for the STAC API Item Search filter.", + "properties": { + "datetime": { + "description": "Datetime", + "type": "string", + "title": "Acquired", + "format": "date-time", + "pattern": "(\\+00:00|Z)$" + }, + "id": { + "title": "Item ID", + "description": "Item identifier", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/definitions/core/allOf/2/properties/id" + }, + "eo:cloud_cover": { + "$ref": "https://stac-extensions.github.io/eo/v1.0.0/schema.json#/definitions/fields/properties/eo:cloud_cover" + }, + "sat:orbit_state": { + "title": "Orbit State", + "$ref": "https://stac-extensions.github.io/sat/v1.0.0/schema.json#/definitions/fields/properties/sat:orbit_state" + }, + "sat:relative_orbit": { + "title": "Relative Orbit", + "$ref": "https://stac-extensions.github.io/sat/v1.0.0/schema.json#/definitions/fields/properties/sat:relative_orbit" + }, + "s2:datatake_type": { + "title": "Datatake Type", + "type": "string", + "enum": [ + "INS-NOBS", + "INS-EOBS", + "INS-DASC", + "INS-ABSR", + "INS-VIC", + "INS-RAW", + "INS-TST" + ] + }, + "s2:mgrs_tile": { + "title": "MGRS Tile", + "type": "string" + }, + "s2:granule_id": { + "title": "Granule ID", + "type": "string" + }, + "s2:product_uri": { + "title": "Product URI", + "type": "string" + }, + "s2:product_type": { + "title": "Product Type", + "type": "string", + "enum": [ + "S2MSI2A" + ] + }, + "s2:mean_solar_zenith": { + "title": "Mean Solar Zenith", + "type": "number" + }, + "s2:mean_solar_azimuth": { + "title": "Mean Solar Azimuth", + "type": "number" + }, + "s2:water_percentage": { + "title": "Water Pct.", + "type": "number", + "minimum": 0, + "maximum": 100 + }, + "s2:snow_ice_percentage": { + "title": "Snow/Ice Pct.", + "type": "number", + "minimum": 0, + "maximum": 100 + }, + "s2:vegetation_percentage": { + "title": "Vegetation Pct.", + "type": "number", + "minimum": 0, + "maximum": 100 + }, + "s2:thin_cirrus_percentage": { + "title": "Thin Cirrus Pct.", + "type": "number", + "minimum": 0, + "maximum": 100 + }, + "s2:cloud_shadow_percentage": { + "title": "Cloud Shadow Pct.", + "type": "number", + "minimum": 0, + "maximum": 100 + }, + "s2:nodata_pixel_percentage": { + "title": "NoData Pct.", + "type": "number", + "minimum": 0, + "maximum": 100 + }, + "s2:dark_features_percentage": { + "title": "Dark Features Pct.", + "type": "number", + "minimum": 0, + "maximum": 100 + }, + "s2:not_vegetated_percentage": { + "title": "Not Vegetated Pct.", + "type": "number", + "minimum": 0, + "maximum": 100 + } + } + }, + "mosaic_info": { + "mosaics": [ + { + "name": "Most recent (low cloud)", + "description": "", + "cql": [ + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Most recent (any cloud cover)", + "description": "", + "cql": [] + }, + { + "name": "Sep \u2013 Nov, 2021 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2021-09-01", + "2021-11-30T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Jun \u2013 Aug, 2021 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2021-06-01", + "2021-08-31T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Mar \u2013 May, 2021 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2021-03-01", + "2021-05-31T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Dec \u2013 Feb, 2021 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2020-12-01", + "2021-02-28T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Sep \u2013 Nov, 2020 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2020-09-01", + "2020-11-30T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Jun \u2013 Aug, 2020 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2020-06-01", + "2020-08-31T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Mar \u2013 May, 2020 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2020-03-01", + "2020-05-31T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Dec \u2013 Feb, 2020 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2019-12-01", + "2020-02-28T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Sep \u2013 Nov, 2019 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2019-09-01", + "2019-11-30T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Jun \u2013 Aug, 2019 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + [ + "2019-06-01", + "2019-08-31T23:59:59Z" + ] + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Mar \u2013 May, 2019 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2019-03-01", + "2019-05-31T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Dec \u2013 Feb, 2019 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2018-12-01", + "2019-02-28T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Sep \u2013 Nov, 2018 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2018-09-01", + "2018-11-30T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Jun \u2013 Aug, 2018 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2018-06-01", + "2018-08-31T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Mar \u2013 May, 2018 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2018-03-01", + "2018-05-31T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + }, + { + "name": "Dec \u2013 Feb, 2018 (low cloud)", + "description": "", + "cql": [ + { + "op": "anyinteracts", + "args": [ + { + "property": "datetime" + }, + { + "interval": [ + "2017-12-01", + "2018-02-28T23:59:59Z" + ] + } + ] + }, + { + "op": "<=", + "args": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + } + ], + "renderOptions": [ + { + "name": "Natural color", + "description": "True color composite of visible bands (B04, B03, B02)", + "options": "assets=B04&assets=B03&assets=B02&nodata=0&color_formula=Gamma RGB 3.7 Saturation 1.5 Sigmoidal RGB 15 0.35", + "minZoom": 9 + }, + { + "name": "Color infrared", + "description": "Highlights healthy (red) and unhealthy (blue/gray) vegetation (B08, B04, B03).", + "options": "assets=B08&assets=B04&assets=B03&nodata=0&color_formula=Gamma RGB 3.7 Saturation 1.5 Sigmoidal RGB 15 0.35", + "minZoom": 9 + }, + { + "name": "Short wave infrared", + "description": "Darker shades of green indicate denser vegetation. Brown is indicative of bare soil and built-up areas (B12, B8A, B04).", + "options": "assets=B12&assets=B8A&assets=B04&nodata=0&color_formula=Gamma RGB 3.7 Saturation 1.5 Sigmoidal RGB 15 0.35", + "minZoom": 9 + }, + { + "name": "Agriculture", + "description": "Darker shades of green indicate denser vegetation (B11, B08, B02).", + "options": "assets=B11&assets=B08&assets=B02&nodata=0&color_formula=Gamma RGB 3.7 Saturation 1.5 Sigmoidal RGB 15 0.35", + "minZoom": 9 + }, + { + "name": "Normalized Difference Veg. Index (NDVI)", + "description": "Normalized Difference Vegetation Index (B08-B04)/(B08+B04), darker green indicates healthier vegetation.", + "options": "nodata=0&expression=(B08-B04)/(B08+B04)&rescale=-1,1&colormap_name=rdylgn", + "minZoom": 9 + }, + { + "name": "Moisture Index (NDWI)", + "description": "Index indicating water stress in plants (B8A-B11)/(B8A+B11)", + "options": "nodata=0&expression=(B8A-B11)/(B8A+B11)&rescale=-1,1&colormap_name=rdbu", + "minZoom": 9 + }, + { + "name": "Atmospheric penetration", + "description": "False color rendering with non-visible bands to reduce effects of atmospheric particles (B12, B11, B8A).", + "options": "nodata=0&assets=B12&assets=B11&assets=B8A&color_formula=Gamma RGB 3.7 Saturation 1.5 Sigmoidal RGB 15 0.35", + "minZoom": 9 + } + ], + "defaultLocation": { + "zoom": 9, + "coordinates": [ + -16.494, + 124.0274 + ] + }, + "defaultCustomQuery": {} + } + } +} \ No newline at end of file diff --git a/pccommon/tests/data-files/container_config.json b/pccommon/tests/data-files/container_config.json new file mode 100644 index 00000000..33a1b53e --- /dev/null +++ b/pccommon/tests/data-files/container_config.json @@ -0,0 +1,5 @@ +{ + "naipeuwest/naip": { + "has_cdn": true + } +} \ No newline at end of file diff --git a/pccommon/tests/test_render.py b/pccommon/tests/test_render.py deleted file mode 100644 index c8e880ea..00000000 --- a/pccommon/tests/test_render.py +++ /dev/null @@ -1,56 +0,0 @@ -import unittest -from urllib.parse import quote_plus - -from pccommon.render import DefaultRenderConfig - -multi_assets = DefaultRenderConfig( - assets=["data1", "data2"], - render_params={"colormap_name": "terrain", "rescale": [-1000, 4000]}, - minzoom=8, -) - -single_asset = DefaultRenderConfig( - assets=["data1"], - render_params={"colormap_name": "terrain", "rescale": [-1000, 4000]}, - minzoom=8, -) - -no_assets = DefaultRenderConfig( - render_params={ - "expression": ("asset1," "0.45*asset2," "asset3/asset1"), - "colormap_name": "terrain", - "rescale": [-1000, 4000], - }, - minzoom=8, -) - - -class TestRenderParams(unittest.TestCase): - def test_multi_asset(self) -> None: - qs = multi_assets.get_full_render_qs("my_collection_id", "my_item_id") - self.assertEqual( - qs, - "collection=my_collection_id&item=my_item_id&assets=data1&assets=data2&colormap_name=terrain&rescale=-1000,4000", - ) - - def test_single_asset(self) -> None: - qs = single_asset.get_full_render_qs("my_collection_id", "my_item_id") - self.assertEqual( - qs, - "collection=my_collection_id&item=my_item_id&assets=data1&colormap_name=terrain&rescale=-1000,4000", - ) - - def test_no_asset(self) -> None: - qs = no_assets.get_full_render_qs("my_collection_id", "my_item_id") - encoded_params = quote_plus("asset1,0.45*asset2,asset3/asset1") - self.assertEqual( - qs, - f"collection=my_collection_id&item=my_item_id&expression={encoded_params}&colormap_name=terrain&rescale=-1000,4000", - ) - - def test_collection_only(self) -> None: - qs = single_asset.get_full_render_qs("my_collection_id") - self.assertEqual( - qs, - "collection=my_collection_id&assets=data1&colormap_name=terrain&rescale=-1000,4000", - ) diff --git a/pccommon/tests/test_tracing.py b/pccommon/tests/test_tracing.py index 95f1c537..086f4692 100644 --- a/pccommon/tests/test_tracing.py +++ b/pccommon/tests/test_tracing.py @@ -1,39 +1,42 @@ -import unittest - from pccommon.tracing import _parse_cqljson + from .data.cql import cql, cql2, cql2_nested, cql2_no_collection, cql_multi -class TestSearchTracing(unittest.TestCase): - def test_tracing(self) -> None: - pass +def test_tracing() -> None: + pass + + +def test_cql_collection_parsing() -> None: + collection_id, item_id = _parse_cqljson(cql) + + assert collection_id == "landsat" + assert item_id == "l8_12345" + + +def test_cql_multi_collection_parsing() -> None: + collection_id, item_id = _parse_cqljson(cql_multi) - def test_cql_collection_parsing(self) -> None: - collection_id, item_id = _parse_cqljson(cql) + collection_id == "landsat,sentinel" + assert item_id is None - self.assertEqual(collection_id, "landsat") - self.assertEqual(item_id, "l8_12345") - def test_cql_multi_collection_parsing(self) -> None: - collection_id, item_id = _parse_cqljson(cql_multi) +def test_cql2_collection_parsing() -> None: + collection_id, item_id = _parse_cqljson(cql2) - self.assertEqual(collection_id, "landsat,sentinel") - self.assertEqual(item_id, None) + assert collection_id == "landsat" + assert item_id is None - def test_cql2_collection_parsing(self) -> None: - collection_id, item_id = _parse_cqljson(cql2) - self.assertEqual(collection_id, "landsat") - self.assertEqual(item_id, None) +def test_cql2_nested_multi_collection_parsing() -> None: + collection_id, item_id = _parse_cqljson(cql2_nested) - def test_cql2_nested_multi_collection_parsing(self) -> None: - collection_id, item_id = _parse_cqljson(cql2_nested) + collection_id == "landsat,sentinel" + item_id == "l8_12345,s2_12345" - self.assertEqual(collection_id, "landsat,sentinel") - self.assertEqual(item_id, "l8_12345,s2_12345") - def test_cql2_no_collection(self) -> None: - collection_id, item_id = _parse_cqljson(cql2_no_collection) +def test_cql2_no_collection() -> None: + collection_id, item_id = _parse_cqljson(cql2_no_collection) - self.assertEqual(collection_id, None) - self.assertEqual(item_id, None) + collection_id is None + item_id is None diff --git a/pcstac/Dockerfile b/pcstac/Dockerfile index 44eb9063..b65f8f89 100644 --- a/pcstac/Dockerfile +++ b/pcstac/Dockerfile @@ -7,23 +7,11 @@ ENV CURL_CA_BUNDLE /etc/ssl/certs/ca-certificates.crt WORKDIR /opt/src -# PCCommon -COPY pccommon/requirements.txt requirements.txt -RUN pip install -r requirements.txt -RUN rm -rf requirements.txt - -# PCStac -COPY pcstac/requirements.txt requirements.txt -RUN pip install -r requirements.txt -RUN rm -rf requirements.txt - -COPY .isort.cfg /opt/src/.isort.cfg COPY pcstac/gunicorn_conf.py gunicorn_conf.py COPY pcstac /opt/src/pcstac COPY pccommon /opt/src/pccommon -# Use --no-deps flag because they were previously installed with requirement.txt -RUN pip install -e ./pccommon -e ./pcstac --no-deps +RUN pip install -e ./pccommon -e ./pcstac[server] ENV APP_HOST=0.0.0.0 ENV APP_PORT=81 diff --git a/pcstac/pcstac/cache.py b/pcstac/pcstac/cache.py index f2ebe4ef..cba9af1c 100644 --- a/pcstac/pcstac/cache.py +++ b/pcstac/pcstac/cache.py @@ -1,9 +1,9 @@ -from cachetools import TTLCache +from cachetools import Cache, TTLCache # A TTL cache, only used for the (large) '/collections' endpoint # TTL set to 600 seconds == 10 minutes -collections_endpoint_cache: TTLCache = TTLCache(maxsize=1, ttl=600) +collections_endpoint_cache: Cache = TTLCache(maxsize=1, ttl=600) # A TTL cache, only used for computed all-collection queryables # TTL set to 21600 seconds == 6 hours -queryables_endpoint_cache: TTLCache = TTLCache(maxsize=1, ttl=21600) +queryables_endpoint_cache: Cache = TTLCache(maxsize=1, ttl=21600) diff --git a/pcstac/pcstac/client.py b/pcstac/pcstac/client.py index 3170ee72..be49d1f2 100644 --- a/pcstac/pcstac/client.py +++ b/pcstac/pcstac/client.py @@ -13,7 +13,7 @@ LandingPage, ) -from pccommon.render import COLLECTION_RENDER_CONFIG +from pccommon.config import get_render_config from pcstac.cache import collections_endpoint_cache from pcstac.config import API_DESCRIPTION, API_LANDING_PAGE_ID, API_TITLE, get_settings from pcstac.search import PCSearch @@ -46,7 +46,7 @@ def conformance_classes(self) -> List[str]: def inject_collection_links(self, collection: Collection) -> Collection: """Add extra/non-mandatory links to a Collection""" collection_id = collection.get("id", "") - render_config = COLLECTION_RENDER_CONFIG.get(collection_id) + render_config = get_render_config(collection_id) if render_config and render_config.should_add_collection_links: TileInfo(collection_id, render_config).inject_collection(collection) @@ -55,7 +55,7 @@ def inject_collection_links(self, collection: Collection) -> Collection: "rel": "describedby", "href": urljoin( "https://planetarycomputer.microsoft.com/dataset/", - collection["id"], + collection_id, ), "title": "Human readable dataset overview and reference", "type": "text/html", @@ -68,7 +68,7 @@ def inject_item_links(self, item: Item) -> Item: """Add extra/non-mandatory links to an Item""" collection_id = item.get("collection", "") if collection_id: - render_config = COLLECTION_RENDER_CONFIG.get(collection_id) + render_config = get_render_config(collection_id) if render_config and render_config.should_add_item_links: TileInfo(collection_id, render_config).inject_item(item) @@ -90,7 +90,8 @@ async def all_collections(self, **kwargs: Dict[str, Any]) -> Collections: collections = await super().all_collections(**kwargs) modified_collections = [] for col in collections.get("collections", []): - render_config = COLLECTION_RENDER_CONFIG.get(col.get("id", "")) + collection_id = col.get("id", "") + render_config = get_render_config(collection_id) if render_config and render_config.hidden: pass else: @@ -114,7 +115,7 @@ async def get_collection( Collection. """ try: - render_config = COLLECTION_RENDER_CONFIG.get(collection_id) + render_config = get_render_config(collection_id) # If there's a configuration and it's set to hidden, # pretend we never found it. diff --git a/pcstac/pcstac/filter.py b/pcstac/pcstac/filter.py index 0943628f..7c1e9732 100644 --- a/pcstac/pcstac/filter.py +++ b/pcstac/pcstac/filter.py @@ -1,21 +1,16 @@ import asyncio -import json from typing import Any, Dict, List, Optional, Set -import requests from fastapi import HTTPException, Request from stac_fastapi.types.core import AsyncBaseFiltersClient +from pccommon.config import get_collection_config from pcstac.cache import queryables_endpoint_cache class MSPCFiltersClient(AsyncBaseFiltersClient): """Defines a pattern for implementing the STAC filter extension.""" - queryable_url_template = ( - "https://planetarycomputer.microsoft.com/stac/{cid}/queryables.json" - ) - async def get_queryable_intersection(self, request: Request) -> dict: """Generate json schema with intersecting properties of all collections. When queryables are requested without specifying a collection (/queryable @@ -53,6 +48,7 @@ async def get_queryable_intersection(self, request: Request) -> dict: property_name_intersection: List[str] = list( set.intersection(*all_property_keys) ) + maybe_match: Optional[str] = None for name in property_name_intersection: for idx, properties in enumerate(all_properties): if idx == 0: @@ -82,21 +78,17 @@ async def get_queryables( under OGC CQL but it is allowed by the STAC API Filter Extension https://github.com/radiantearth/stac-api-spec/tree/master/fragments/filter#queryables """ + queryable_resp: Dict[str, Any] if not collection_id: try: queryable_resp = queryables_endpoint_cache["/queryables"] except KeyError: - request = kwargs["request"] - if isinstance(request, Request): - queryable_resp = await self.get_queryable_intersection(request) - queryables_endpoint_cache["/queryables"] = queryable_resp + request: Request = kwargs["request"] # type: ignore + queryable_resp = await self.get_queryable_intersection(request) + queryables_endpoint_cache["/queryables"] = queryable_resp else: - r = requests.get(self.queryable_url_template.format(cid=collection_id)) - if r.status_code == 404: + collection_config = get_collection_config(collection_id) + if not collection_config or not collection_config.queryables: raise HTTPException(status_code=404) - elif r.status_code == 200: - try: - queryable_resp = r.json() - except json.decoder.JSONDecodeError: - raise HTTPException(status_code=404) + queryable_resp = collection_config.queryables return queryable_resp diff --git a/pcstac/pcstac/tiles.py b/pcstac/pcstac/tiles.py index 8653694b..a1be60cb 100644 --- a/pcstac/pcstac/tiles.py +++ b/pcstac/pcstac/tiles.py @@ -4,7 +4,7 @@ import pystac from stac_fastapi.types.stac import Collection, Item -from pccommon.render import DefaultRenderConfig +from pccommon.config.collections import DefaultRenderConfig from pcstac.config import get_settings TILER_HREF = get_settings().tiler_href diff --git a/pcstac/requirements.txt b/pcstac/requirements.txt deleted file mode 100644 index 15a0770a..00000000 --- a/pcstac/requirements.txt +++ /dev/null @@ -1,24 +0,0 @@ -# pccommon deps -fastapi==0.67.* -opencensus-ext-azure==1.0.8 -opencensus-ext-logging==0.1.0 - -# server deps -uvicorn[standard]==0.13.3 -uvloop==0.14.0 -gunicorn==20.1.0 - -orjson==3.5.2 -cachetools==4.2.1 - -stac-fastapi.types==2.3.0 -stac-fastapi.api==2.3.0 -stac-fastapi.extensions==2.3.0 -stac-fastapi.pgstac==2.3.0 - -pystac==1.* - -# TODO: remove, pypgstac is not really needed to run stac-fastapi application -pypgstac==0.4.3 - -# pccommon diff --git a/pcstac/setup.py b/pcstac/setup.py index de5bfe9c..4934b28a 100644 --- a/pcstac/setup.py +++ b/pcstac/setup.py @@ -3,8 +3,16 @@ from setuptools import find_packages, setup # Runtime requirements. -with open("requirements.txt", "r") as f: - inst_reqs = f.read() +inst_reqs = [ + "stac-fastapi.types==2.3.0", + "stac-fastapi.api==2.3.0", + "stac-fastapi.extensions==2.3.0", + "stac-fastapi.pgstac==2.3.0", + "pystac==1.*", + "pccommon", + # TODO: remove, pypgstac is not really needed to run stac-fastapi application + "pypgstac==0.4.3", +] extra_reqs = { "test": [ diff --git a/pcstac/tests/loadtestdata.py b/pcstac/tests/loadtestdata.py index 7ea187ad..15347d89 100644 --- a/pcstac/tests/loadtestdata.py +++ b/pcstac/tests/loadtestdata.py @@ -1,5 +1,5 @@ -import os import asyncio +import os from pathlib import Path import orjson diff --git a/pcstac/tests/resources/test_queryables.py b/pcstac/tests/resources/test_queryables.py index 4c75a263..c19f8092 100644 --- a/pcstac/tests/resources/test_queryables.py +++ b/pcstac/tests/resources/test_queryables.py @@ -1,10 +1,19 @@ -from typing import Callable - import pytest @pytest.mark.asyncio -async def test_queryables(app_client, load_test_data: Callable): +async def test_queryables(app_client): + resp = await app_client.get("/queryables") + assert resp.status_code == 200 + properties = resp.json()["properties"] + assert "id" in properties + assert "datetime" in properties + assert "naip:year" in properties + assert "naip:state" in properties + + +@pytest.mark.asyncio +async def test_queryables_io_lulc(app_client): resp = await app_client.get("/queryables") assert resp.status_code == 200 properties = resp.json()["properties"] @@ -15,7 +24,17 @@ async def test_queryables(app_client, load_test_data: Callable): @pytest.mark.asyncio -async def test_collection_queryables(app_client, load_test_data: Callable): +async def test_collection_queryables_io_lulc(app_client): + resp = await app_client.get("/collections/io-lulc/queryables") + assert resp.status_code == 200 + properties = resp.json()["properties"] + assert "id" in properties + assert "datetime" in properties + assert "io:supercell_id" in properties + + +@pytest.mark.asyncio +async def test_collection_queryables_naip(app_client): resp = await app_client.get("/collections/naip/queryables") assert resp.status_code == 200 properties = resp.json()["properties"] @@ -26,6 +45,6 @@ async def test_collection_queryables(app_client, load_test_data: Callable): @pytest.mark.asyncio -async def test_collection_queryables_404(app_client, load_test_data: Callable): +async def test_collection_queryables_404(app_client): resp = await app_client.get("/collections/does-not-exist/queryables") assert resp.status_code == 404 diff --git a/pctiler/Dockerfile b/pctiler/Dockerfile index 5a1f124a..cbd50bf0 100644 --- a/pctiler/Dockerfile +++ b/pctiler/Dockerfile @@ -20,23 +20,11 @@ RUN pip install --upgrade pip setuptools wheel WORKDIR /opt/src -COPY .isort.cfg /opt/src/.isort.cfg COPY pctiler/gunicorn_conf.py gunicorn_conf.py -# PCCommon -COPY pccommon/requirements.txt requirements.txt -RUN pip install -r requirements.txt -RUN rm -rf requirements.txt - -# PCTiler -COPY pctiler/requirements.txt requirements.txt -RUN pip install -r requirements.txt -RUN rm -rf requirements.txt - COPY pccommon /opt/src/pccommon COPY pctiler /opt/src/pctiler -# Use --no-deps flag because they were previously installed with requirement.txt -RUN pip install -e ./pccommon -e ./pctiler --no-deps +RUN pip install -e ./pccommon -e ./pctiler[server] # GDAL config ENV GDAL_CACHEMAX 200 diff --git a/pctiler/pctiler/collections.py b/pctiler/pctiler/collections.py index 9dcb06bc..1056db3b 100644 --- a/pctiler/pctiler/collections.py +++ b/pctiler/pctiler/collections.py @@ -1,9 +1,11 @@ from dataclasses import dataclass +from functools import partial from typing import Any, Dict, Set from urllib.parse import urljoin import requests -from cachetools import TTLCache, cached +from cachetools import Cache, TTLCache, cachedmethod +from cachetools.keys import hashkey from fastapi.exceptions import HTTPException from pccommon.backoff import with_backoff @@ -54,8 +56,10 @@ class Collections: TODO: Make this async """ + _cache: Cache = TTLCache(maxsize=1, ttl=600) + @classmethod - @cached(cache=TTLCache(maxsize=1, ttl=600)) + @cachedmethod(cache=lambda self: self._cache, key=partial(hashkey, "collections")) def get_collections(cls) -> Dict[str, CollectionInfo]: href = urljoin(get_settings().stac_api_url, "collections") collections = with_backoff(lambda: requests.get(href).json()["collections"]) @@ -68,7 +72,7 @@ def get_collections(cls) -> Dict[str, CollectionInfo]: } @classmethod - @cached(cache=TTLCache(maxsize=1, ttl=600)) + @cachedmethod(cache=lambda self: self._cache, key=partial(hashkey, "storage")) def get_storage_set(cls) -> Dict[str, Set[str]]: """Returns information about what storage accounts and containers are available. diff --git a/pctiler/pctiler/config.py b/pctiler/pctiler/config.py index 62144f53..7cbd9413 100644 --- a/pctiler/pctiler/config.py +++ b/pctiler/pctiler/config.py @@ -21,9 +21,7 @@ class Settings(BaseSettings): title: str = "Preview of Tile Access Services" openapi_url: str = "/openapi.json" - ogc_endpoint_prefix: str = "/asset-tiles" item_endpoint_prefix: str = "/item" - collection_endpoint_prefix: str = "/collection" mosaic_endpoint_prefix: str = "/mosaic" legend_endpoint_prefix: str = "/legend" debug: bool = os.getenv("TILER_DEBUG", "False").lower() == "true" diff --git a/pctiler/pctiler/endpoints/item.py b/pctiler/pctiler/endpoints/item.py index 4a18912d..d1f3a282 100644 --- a/pctiler/pctiler/endpoints/item.py +++ b/pctiler/pctiler/endpoints/item.py @@ -6,7 +6,7 @@ from starlette.responses import HTMLResponse from titiler.core.factory import MultiBaseTilerFactory -from pccommon.render import COLLECTION_RENDER_CONFIG +from pccommon.config import get_render_config from pctiler.colormaps import PCColorMapParams from pctiler.config import get_settings from pctiler.reader import ItemSTACReader @@ -53,7 +53,7 @@ def map( collection: str = Query(..., description="STAC Collection ID"), item: str = Query(..., description="STAC Item ID"), ) -> Response: - render_config = COLLECTION_RENDER_CONFIG.get(collection) + render_config = get_render_config(collection) if render_config is None: return Response( status_code=404, diff --git a/pctiler/pctiler/endpoints/legend.py b/pctiler/pctiler/endpoints/legend.py index 57762b16..9576bf46 100644 --- a/pctiler/pctiler/endpoints/legend.py +++ b/pctiler/pctiler/endpoints/legend.py @@ -4,17 +4,17 @@ import matplotlib.pyplot as plt import numpy as np from fastapi import APIRouter, HTTPException +from fastapi.responses import ORJSONResponse, Response from matplotlib.colors import ListedColormap from rio_tiler.colormap import make_lut -from starlette.responses import JSONResponse, Response from ..colormaps import custom_colormaps, registered_cmaps legend_router = APIRouter() -@legend_router.get("/classmap/{classmap_name}") -async def get_classmap_legend(classmap_name: str) -> JSONResponse: +@legend_router.get("/classmap/{classmap_name}", response_class=ORJSONResponse) +async def get_classmap_legend(classmap_name: str) -> ORJSONResponse: """Generate values and color swatches mapping for a given classmap.""" classmap = custom_colormaps.get(classmap_name) @@ -23,7 +23,7 @@ async def get_classmap_legend(classmap_name: str) -> JSONResponse: status_code=404, detail=f"Classmap {classmap_name} not found" ) - return JSONResponse(content=classmap) + return ORJSONResponse(content=classmap) @legend_router.get("/colormap/{cmap_name}", response_class=Response) diff --git a/pctiler/pctiler/endpoints/pg_mosaic.py b/pctiler/pctiler/endpoints/pg_mosaic.py index 06130ebf..48641c8d 100644 --- a/pctiler/pctiler/endpoints/pg_mosaic.py +++ b/pctiler/pctiler/endpoints/pg_mosaic.py @@ -1,9 +1,12 @@ from dataclasses import dataclass -from fastapi import Query +from fastapi import Query, Request +from fastapi.responses import ORJSONResponse from titiler.core import dependencies from titiler.pgstac.factory import MosaicTilerFactory +from pccommon.config import get_collection_config +from pccommon.config.collections import MosaicInfo from pctiler.colormaps import PCColorMapParams from pctiler.config import get_settings from pctiler.reader import PGSTACBackend @@ -21,3 +24,22 @@ class AssetsBidxExprParams(dependencies.AssetsBidxExprParams): layer_dependency=AssetsBidxExprParams, router_prefix=get_settings().mosaic_endpoint_prefix, ) + + +@pgstac_mosaic_factory.router.get( + "/info", response_model=MosaicInfo, response_class=ORJSONResponse +) +def map( + request: Request, collection: str = Query(..., description="STAC Collection ID") +) -> ORJSONResponse: + collection_config = get_collection_config(collection) + if not collection_config or not collection_config.mosaic_info: + return ORJSONResponse( + status_code=404, + content=f"No mosaic info available for collection {collection}", + ) + + return ORJSONResponse( + status_code=200, + content=collection_config.mosaic_info.dict(by_alias=True, exclude_unset=True), + ) diff --git a/pctiler/pctiler/reader.py b/pctiler/pctiler/reader.py index dee67119..4e1842d3 100644 --- a/pctiler/pctiler/reader.py +++ b/pctiler/pctiler/reader.py @@ -19,12 +19,19 @@ from titiler.pgstac import mosaic as pgstac_mosaic from titiler.pgstac.settings import CacheSettings -from pccommon.render import COLLECTION_RENDER_CONFIG, BlobCDN +from pccommon.cdn import BlobCDN +from pccommon.config import get_render_config from pctiler.reader_cog import CustomCOGReader # type:ignore cache_config = CacheSettings() +def get_cache_key( + backend: "PGSTACBackend", geom: Union[Point, Polygon], **kwargs: Any +) -> Any: + return hashkey(backend.input, str(geom), **kwargs) + + @attr.s class ItemSTACReader(STACReader): @@ -35,7 +42,7 @@ def _get_asset_url(self, asset: str) -> str: asset_url = BlobCDN.transform_if_available(super()._get_asset_url(asset)) if self.item.collection_id: - render_config = COLLECTION_RENDER_CONFIG.get(self.item.collection_id) + render_config = get_render_config(self.item.collection_id) if render_config and render_config.requires_token: asset_url = pc.sign(asset_url) @@ -99,7 +106,7 @@ def _get_asset_url(self, asset: str) -> str: collection = self.input.get("collection", None) if collection: - render_config = COLLECTION_RENDER_CONFIG.get(collection) + render_config = get_render_config(collection) if render_config and render_config.requires_token: asset_url = pc.sign(asset_url) @@ -124,7 +131,7 @@ def assets_for_tile( ) # Check that the zoom isn't lower than minZoom - render_config = COLLECTION_RENDER_CONFIG.get(collection) + render_config = get_render_config(collection) if render_config and render_config.minzoom and render_config.minzoom > z: return [] @@ -132,8 +139,8 @@ def assets_for_tile( return self.get_assets(Polygon.from_bounds(*bbox), **kwargs) @cached( - TTLCache(maxsize=cache_config.maxsize, ttl=cache_config.ttl), - key=lambda self, geom, **kwargs: hashkey(self.input, str(geom), **kwargs), + cache=TTLCache(maxsize=cache_config.maxsize, ttl=cache_config.ttl), + key=get_cache_key, ) def get_assets( self, diff --git a/pctiler/requirements.txt b/pctiler/requirements.txt deleted file mode 100644 index 29c6e3fd..00000000 --- a/pctiler/requirements.txt +++ /dev/null @@ -1,23 +0,0 @@ -geojson-pydantic==0.3.1 -jinja2==2.11.3 -cachetools==4.2.1 -pystac==1.0.0-rc.2 -planetary-computer==0.3.0-rc.0 - -psycopg[binary,pool] - -rasterio==1.2.6 -titiler.core==0.4.* -titiler.mosaic==0.4.* -titiler.pgstac==0.1.0a4 -matplotlib==3.4.* - -uvicorn[standard]==0.13.3 -uvloop==0.14.0 -gunicorn==20.1.0 - -azure-identity==1.5.0 -azure-storage-blob==12.6.0 - -opencensus-ext-azure==1.0.8 -opencensus-ext-logging==0.1.0 diff --git a/pctiler/setup.py b/pctiler/setup.py index e4292afd..c41eb7a2 100644 --- a/pctiler/setup.py +++ b/pctiler/setup.py @@ -6,11 +6,10 @@ inst_reqs = [ "geojson-pydantic==0.3.1", "jinja2==2.11.3", - "cachetools==4.2.1", - "pystac==1.0.0-rc.2", - "planetary-computer==0.3.0-rc.0", + "pystac==1.*", + "planetary-computer==0.4.*", - "rasterio==1.2.6", + "rasterio==1.2.*", "titiler.core==0.4.*", "titiler.mosaic==0.4.*", diff --git a/pctiler/tests/conftest.py b/pctiler/tests/conftest.py new file mode 100644 index 00000000..147e9e9c --- /dev/null +++ b/pctiler/tests/conftest.py @@ -0,0 +1,9 @@ +import pytest +from fastapi.testclient import TestClient + +from pctiler.main import app + + +@pytest.fixture +def client() -> TestClient: + return TestClient(app) diff --git a/pctiler/tests/endpoints/__init__.py b/pctiler/tests/endpoints/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pctiler/tests/endpoints/test_pg_mosaic.py b/pctiler/tests/endpoints/test_pg_mosaic.py new file mode 100644 index 00000000..2c1ccc57 --- /dev/null +++ b/pctiler/tests/endpoints/test_pg_mosaic.py @@ -0,0 +1,11 @@ +from fastapi.testclient import TestClient + +from pccommon.config.collections import MosaicInfo + + +def test_get(client: TestClient) -> None: + response = client.get("/mosaic/info?collection=naip") + assert response.status_code == 200 + info_dict = response.json() + mosaic_info = MosaicInfo(**info_dict) + assert mosaic_info.default_location.zoom == 13 diff --git a/pctiler/tests/test_openapi.py b/pctiler/tests/test_openapi.py index e505e4b8..ef7909de 100644 --- a/pctiler/tests/test_openapi.py +++ b/pctiler/tests/test_openapi.py @@ -1,5 +1,3 @@ -import unittest - from fastapi.testclient import TestClient from openapi_spec_validator import validate_spec from requests.models import Response @@ -9,23 +7,22 @@ VALIDATING_SCHEMA = False -class TilerOpenAPITest(unittest.TestCase): - def test_produces_valid_openapi_spec(self) -> None: - with TestClient(app) as client: - """ - When the request supplies an origin header (as a browser would), ensure - that the response has an `access-control-allow` header, set to all origins. - """ - response: Response = client.get( - "/openapi.json", headers={"origin": "http://example.com"} - ) +def test_produces_valid_openapi_spec() -> None: + with TestClient(app) as client: + """ + When the request supplies an origin header (as a browser would), ensure + that the response has an `access-control-allow` header, set to all origins. + """ + response: Response = client.get( + "/openapi.json", headers={"origin": "http://example.com"} + ) - self.assertEqual(response.status_code, 200) - spec_json = response.json() + assert response.status_code == 200 + spec_json = response.json() - # Titiler is injecting some invalid schema - - # something around Asset - # Since we aren't importing into API Management, - # deal with the invalid openapi for now. - if VALIDATING_SCHEMA: - validate_spec(spec_json) + # Titiler is injecting some invalid schema - + # something around Asset + # Since we aren't importing into API Management, + # deal with the invalid openapi for now. + if VALIDATING_SCHEMA: + validate_spec(spec_json) diff --git a/pctiler/tests/test_routes.py b/pctiler/tests/test_routes.py index ed9b7f0a..dcb496e5 100644 --- a/pctiler/tests/test_routes.py +++ b/pctiler/tests/test_routes.py @@ -1,24 +1,16 @@ -import unittest - from fastapi.testclient import TestClient -from pctiler.main import app - -client = TestClient(app) - -class TestRoot(unittest.TestCase): - def test_get(self) -> None: - response = client.get("/") - assert response.status_code == 200 - assert response.json() == {"Hello": "Planetary Developer!"} +def test_get(client: TestClient) -> None: + response = client.get("/") + assert response.status_code == 200 + assert response.json() == {"Hello": "Planetary Developer!"} -class TestHealth(unittest.TestCase): - def test_ping_no_param(self) -> None: - """ - Test ping endpoint with a mocked client. - """ - res = client.get("/_mgmt/ping") - assert res.status_code == 200 - assert res.json() == {"message": "PONG"} +def test_ping_no_param(client: TestClient) -> None: + """ + Test ping endpoint with a mocked client. + """ + res = client.get("/_mgmt/ping") + assert res.status_code == 200 + assert res.json() == {"message": "PONG"} diff --git a/scripts/bin/format-common b/scripts/bin/format-common new file mode 100755 index 00000000..aeea85f8 --- /dev/null +++ b/scripts/bin/format-common @@ -0,0 +1,25 @@ +#!/bin/bash + +set -e + +if [[ "${CI}" ]]; then + set -x +fi + +function usage() { + echo -n \ + "Usage: $(basename "$0") +Runs formatting for the common project. + +This scripts is meant to be run inside the stac-dev container. + +" +} + +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + echo "Formatting common..." + isort --overwrite-in-place pccommon/pccommon + isort --overwrite-in-place pccommon/tests + black pccommon/pccommon + black pccommon/tests +fi diff --git a/scripts/bin/format-stac b/scripts/bin/format-stac index f2c70f23..7d2230b9 100755 --- a/scripts/bin/format-stac +++ b/scripts/bin/format-stac @@ -9,7 +9,7 @@ fi function usage() { echo -n \ "Usage: $(basename "$0") -Runs formatting for the pcstac project, as well as pccommon. +Runs formatting for the pcstac project. This scripts is meant to be run inside the stac-dev container. diff --git a/scripts/bin/setup_azurite.py b/scripts/bin/setup_azurite.py new file mode 100644 index 00000000..8222b732 --- /dev/null +++ b/scripts/bin/setup_azurite.py @@ -0,0 +1,85 @@ +#!/bin/bash +""" +Sets up the Azurite development environment. +Meant to execute in a pctasks development docker container. +""" + +import json +from pathlib import Path + +from azure.data.tables import TableServiceClient + +import pccommon +from pccommon.constants import ( + DEFAULT_CONTAINER_CONFIG_TABLE_NAME, + DEFAULT_COLLECTION_CONFIG_TABLE_NAME, +) +from pccommon.config.collections import CollectionConfig, CollectionConfigTable +from pccommon.config.containers import ContainerConfig, ContainerConfigTable + +TEST_DATA_DIR = Path(pccommon.__file__).parent.parent / "tests" / "data-files" +COLLECTION_CONFIG_PATH = TEST_DATA_DIR / "collection_config.json" +CONTAINER_CONFIG_PATH = TEST_DATA_DIR / "container_config.json" +ADDITIONAL_CONFIG_PATH = TEST_DATA_DIR / "additional-config" + +AZURITE_CONNECT_STRING = ( + "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;" + "AccountKey=" + "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq" + "/K1SZFPTOtr/KBHBeksoGMGw==;" + "BlobEndpoint=http://azurite:10000/devstoreaccount1;" + "QueueEndpoint=http://azurite:10001/devstoreaccount1;" + "TableEndpoint=http://azurite:10002/devstoreaccount1;" +) + + +def setup_azurite() -> None: + # Tables + + print("~ Setting up tables...") + + table_service_client = TableServiceClient.from_connection_string( + AZURITE_CONNECT_STRING + ) + tables = [t.name for t in table_service_client.list_tables()] + for table in [ + DEFAULT_CONTAINER_CONFIG_TABLE_NAME, + DEFAULT_COLLECTION_CONFIG_TABLE_NAME, + ]: + if table not in tables: + print(f"~ ~ Creating table {table}...") + table_service_client.create_table(table) + + print("~ ~ Writing container configurations...") + + container_config_table = ContainerConfigTable( + lambda: ( + None, + table_service_client.get_table_client(DEFAULT_CONTAINER_CONFIG_TABLE_NAME), + ) + ) + with open(CONTAINER_CONFIG_PATH) as f: + js = json.load(f) + for container_path, config_dict in js.items(): + sa, container = container_path.split("/") + container_config_table.set_config(sa, container, ContainerConfig(**config_dict)) + + print("~ ~ Writing collection configurations...") + + collection_config_table = CollectionConfigTable( + lambda: ( + None, + table_service_client.get_table_client(DEFAULT_COLLECTION_CONFIG_TABLE_NAME), + ) + ) + with open(COLLECTION_CONFIG_PATH) as f: + js = json.load(f) + for collection_id, config_dict in js.items(): + config = CollectionConfig(**config_dict) + collection_config_table.set_config(collection_id, config) + + print("~ Done Azurite setup.") + + +if __name__ == "__main__": + setup_azurite() diff --git a/scripts/bin/test-common b/scripts/bin/test-common new file mode 100755 index 00000000..8b1648de --- /dev/null +++ b/scripts/bin/test-common @@ -0,0 +1,32 @@ +#!/bin/bash + +set -e + +if [[ "${CI}" ]]; then + set -x +fi + +function usage() { + echo -n \ + "Usage: $(basename "$0") +Runs tests for the common project. + +This scripts is meant to be run inside the stac-dev container. + +" +} + +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + + echo "Running mypy for common..." + mypy pccommon + + echo "Running black for common..." + black --check pccommon + + echo "Running flake8 for common..." + flake8 pccommon/pccommon pccommon/tests + + echo "Running unit tests for common..." + python -m pytest pccommon/tests +fi diff --git a/scripts/bin/test-stac b/scripts/bin/test-stac index b8b704e7..1e5b0d26 100755 --- a/scripts/bin/test-stac +++ b/scripts/bin/test-stac @@ -11,8 +11,6 @@ function usage() { "Usage: $(basename "$0") Runs tests for the STAC service project. -Also checks formatting and runs mypy for the common code. - This scripts is meant to be run inside the stac-dev container. " @@ -20,24 +18,16 @@ This scripts is meant to be run inside the stac-dev container. if [ "${BASH_SOURCE[0]}" = "${0}" ]; then - echo "Running mypy for stac..." + echo "Running mypy for stac..." mypy pcstac/pcstac - echo "Running mypy for common..." - mypy pccommon - echo "Running black for stac..." black --check pcstac/pcstac pcstac/tests - echo "Running black for common..." - black --check pccommon - echo "Running flake8 for stac..." flake8 pcstac/pcstac pcstac/tests echo "Running unit tests for stac..." python -m pytest pcstac/tests - echo "Running unit tests for common..." - python -m pytest pccommon/tests fi diff --git a/scripts/bin/test-tiler b/scripts/bin/test-tiler index fa49374b..6166841b 100755 --- a/scripts/bin/test-tiler +++ b/scripts/bin/test-tiler @@ -27,6 +27,6 @@ if [ "${BASH_SOURCE[0]}" = "${0}" ]; then flake8 pctiler/pctiler pctiler/tests echo "Running unit tests for tiler..." - python -m unittest discover pctiler/tests + python -m pytest pctiler/tests fi diff --git a/scripts/env b/scripts/env new file mode 100755 index 00000000..562a7c2c --- /dev/null +++ b/scripts/env @@ -0,0 +1,7 @@ +#!/bin/bash + +PACKAGE_DIRS=( + "pccommon" + "pcstac" + "pctiler" +) diff --git a/scripts/format b/scripts/format index c40b1087..7af7caa0 100755 --- a/scripts/format +++ b/scripts/format @@ -15,6 +15,13 @@ Runs formatting for the project. } if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + + docker-compose \ + -f docker-compose.yml \ + -f docker-compose.dev.yml \ + run --rm \ + stac-dev scripts/bin/format-common; + docker-compose \ -f docker-compose.yml \ -f docker-compose.dev.yml \ diff --git a/scripts/install b/scripts/install new file mode 100755 index 00000000..00a1c746 --- /dev/null +++ b/scripts/install @@ -0,0 +1,37 @@ +#!/bin/bash + +set -e + +if [[ "${CI}" ]]; then + set -x +fi + +function usage() { + echo -n \ + "Usage: $(basename "$0") [submit|ingest|func|deploy] +Installs python projects into local environment. +" +} + +while [[ $# -gt 0 ]]; do case $1 in + --help) + usage + exit 0 + shift + ;; + *) + usage "Unknown parameter passed: $1" + shift + shift + ;; + esac done + +source scripts/env + +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + for DIR in "${PACKAGE_DIRS[@]}"; do + echo "Installing ${DIR}" + pip install -e ${DIR} + done + +fi diff --git a/scripts/setup b/scripts/setup index 747373de..88804290 100755 --- a/scripts/setup +++ b/scripts/setup @@ -33,6 +33,15 @@ if [ "${BASH_SOURCE[0]}" = "${0}" ]; then stac \ python /opt/src/pcstac/tests/loadtestdata.py + echo "Setting up azurite..." + + docker-compose \ + -f docker-compose.yml \ + -f docker-compose.dev.yml \ + run --rm \ + stac-dev \ + python /opt/src/scripts/bin/setup_azurite.py + echo "Done." fi diff --git a/scripts/test b/scripts/test index 24562cb2..a661ec2c 100755 --- a/scripts/test +++ b/scripts/test @@ -19,6 +19,8 @@ Options Only test pcstac --tiler Only test pctiler + --common + Only test pccommon " } @@ -32,6 +34,10 @@ while [[ $# -gt 0 ]]; do case $1 in TILER_ONLY="1" shift ;; + --common) + COMMON_ONLY="1" + shift + ;; --help) usage exit 0 @@ -44,7 +50,17 @@ while [[ $# -gt 0 ]]; do case $1 in esac done if [ "${BASH_SOURCE[0]}" = "${0}" ]; then - if [ -z ${STAC_ONLY} ]; then + + if [ -z "${TILER_ONLY}${STAC_ONLY}" ]; then + + docker-compose \ + -f docker-compose.yml \ + -f docker-compose.dev.yml \ + run --rm \ + stac-dev scripts/bin/test-common + fi + + if [ -z "${STAC_ONLY}${COMMON_ONLY}" ]; then docker-compose \ -f docker-compose.yml \ -f docker-compose.dev.yml \ @@ -52,7 +68,7 @@ if [ "${BASH_SOURCE[0]}" = "${0}" ]; then tiler-dev scripts/bin/test-tiler fi - if [ -z ${TILER_ONLY} ]; then + if [ -z "${TILER_ONLY}${COMMON_ONLY}" ]; then docker-compose \ -f docker-compose.yml \